Still working on Lever-Spike connection. I was right not to panic yesterday, the snag wasn’t a snag.

Hello, loves.

I was concerned yesterday that the Animated item (in its role as Spikes) was going to be hearing its own messages. I had not realized that the caller ID feature would handle that. The Lever will publish state with the caller ID ‘spikes’, and the Spikes will subscribe to that. The Spikes will publish state using caller ID ‘view’, and we will not subscribe to that. So I was right not to panic. Go, me!

We are engaged in making the spikes behave differently depending on the state of the lever. Sometimes they cycle, sometimes they stop in the up position, sometimes down. We will also need them to do some damage to poor Innocent Intrepid Adventurer Dot.

We’ll begin by making the spikes just stop cycling on hearing any non-zero state from the Lever. Let’s see what the class looks like now:

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]
        return cls(name, resources)

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

    def interact_with_player(self, dungeon):
        pass

    def run(self, dungeon):
        self.state = 0
        self.time = 0
        def callback(delta_time):
            self.time += delta_time
            if self.time >= 1:
                self.time = 0
                self.state = (self.state+1)%len(self.resources)
                dungeon.publish('state_number', self.name, self, self.state)

        dungeon.subscribe('on_update', 'view', callback)
        def control_changed(control, state_number):
            pass

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

There is a bug here. We should be publishing ‘state_number’ to ‘view’, not name, there under the if statement. But the spikes do alternate, which tells me that the view sees that update, because it is the view that changes the pictures. What’s up with that? Ah, this is up with that:

class DungeonView:
    def subscribe_to_state_number(self, dungeon):
        def callback(item, state_number):
            self.view_do(item, lambda view: view.set_state(state_number))

        dungeon.subscribe('state_number', '', callback)

Apparently, the rule is that the view subscribes to all state changes, as indicated by the empty string in caller_id, the second parameter in subscribe. I think that’s probably OK, and actually convenient.

However
This publish-subscribe thing is more than a little tricky to think about. I believe we do need it, and that explicit links between objects would be worse. But let’s try to stay alert to this trickiness and see whether we can come up with ideas to make it easier to think about.

Anyway, we’re here to turn off the cycling of spikes if we get a non-zero state number in our control_changed callback. So we need a cycling flag, and to honor it, and to set it.

Let’s see if we can TDD some or all of that. We built that test file already so let’s see if we can use it. I’ll feel both more confident and a little bit less dirty with tests around this conditional behavior.

First this much, to drive out the flag:

    def test_stops_cycling(self):
        pub_sub = PubSub()
        spikes = Animated.spikes('spikes')
        spikes.run(pub_sub)
        assert spikes.cycling is True
        pub_sub.publish('state_number', 'spikes', self, 1)
        assert spikes.cycling is False

Test fails, no cycling. Add it. I went hog wild and did it all because it was in my head and I’d rather have it in the code:

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

    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.state = (self.state+1)%len(self.resources)
                dungeon.publish('state_number', self.name, self, self.state)

        dungeon.subscribe('on_update', 'view', callback)
        def control_changed(control, state_number):
            self.cycling = False

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

Test goes green. Check the game to see if it has really stopped cycling, though we might be able to test that as well.

Game does not work. Why? Because I didn’t check state_number for non-zero, and we do get a zero state message at the beginning of things.

Oh! The bug is that I put the self.name back when I publish my own state, and that needs to say view so that I won’t see it. With that done, cycling stops as soon as Dot steps on the lever. Now let’s put in the logic we really want. 0 cycles, 1 stops up, 2 stops down, 3 cycles.

Darn. I went ahead and did the code, instead of testing. Tiny fool! Anyway here’s the code, working:

def control_changed(control, state_number):
    match state_number:
        case 0 | 3:
            self.cycling = True
        case 1:
            self.cycling = False
            self.state = 1
            dungeon.publish('state_number', 'view', self, self.state)
        case 2:
            self.cycling = False
            self.state = 0
            dungeon.publish('state_number', 'view', self, self.state)

A bit of duplication there, we’ll fix that quickly but let’s do the testing:

    def test_stops_cycling(self):
        pub_sub = PubSub()
        spikes = Animated.spikes('spikes')
        spikes.run(pub_sub)
        pub_sub.publish('state_number', 'spikes', self, 0)
        assert spikes.cycling is True
        pub_sub.publish('state_number', 'spikes', self, 1)
        assert spikes.cycling is False
        assert spikes.state == 1
        pub_sub.publish('state_number', 'spikes', self, 2)
        assert spikes.cycling is False
        assert spikes.state == 0
        pub_sub.publish('state_number', 'spikes', self, 3)
        assert spikes.cycling is True

Test is green.

Now the duplication. We’ll just have a little method set_state. I wanted to do this with machine refactorings, but PyCharm doesn’t realize I really want a method and tries to give me a local function. So we’ll just code it:

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

And use it throughout:

    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((self.state + 1) % len(self.resources), dungeon)

        dungeon.subscribe('on_update', 'view', callback)
        def control_changed(control, state_number):
            match state_number:
                case 0 | 3:
                    self.cycling = True
                case 1:
                    self.cycling = False
                    self.set_state(1, dungeon)
                case 2:
                    self.cycling = False
                    self.set_state(0, dungeon)

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

Tests are green, game runs right. Long overdue commit: Animated(spikes) responds as requested to control messages from DungeonControl(lever).

Coulda shoulda committed a couple of times before that, but we’re here now.

Reflection

Testing

I didn’t make great use of the ability to test, but we do have tests for the object as it stands.

Generality
There will probably be a request to configure the mapping between control position and spike behavior, and we’ll deal with that when we get it. Probably something like a list of pairs from control state to flag and state, or maybe something more like a mapping from control state to actual behavior.
Animated?
The class we have is called Animated but we only have the spikes example. We do have the nice class method spikes, but the actual code of the object doesn’t seem terribly general, especially with the hard-coded response to controls. I think we’ll just wait to see if another kind of animated thing comes along. We currently have no good reason to change this further.
Damage
We need to Deal Damage to Dot, which is starting to seem to me to be unnecessarily violent in these times of stress. Perhaps we’ll Inconvenience Intrepid Adventurer Dot.

How about this? We’ll make the rule be that when the spikes are cycling or up, Dot cannot cross them. Then we’ll use them to block paths or treasures. We can do similar things with monsters, so that battling them keeps Dot from moving until they are defeated, but does no actual harm.

Silly? Probably. But I just don’t feel like torturing even a simple Dot. There’s enough pain in the world. Let’s not add to it.

Summary

Again, and again and again, we think of a feature that our game needs, and we find simple tests and simple code to put that feature in. Again, and again and again, we improve the code, so that adding features doesn’t get things too tangled up and messy, keeping us able to make good progress.

And that, my friends, is the point.