Hello, loves!

This morning we made something work. Will it support everything we now need? Why didn’t I ask that first?

I didn’t ask that first because I felt that my initial idea for a single pluggable method was tricky enough and that what we’d learn would likely be useful even if the starting idea wasn’t strong enough. And it was tricky, especially now, with the MethodType feature in place, which allows us to pass in behavior fetched either from the class or an instance.

Now, we’ll look at the existing Content classes and see what’s in there, if anything, that can’t be done with the current scheme, or where the current scheme seems to offer too little support. We’ll have to extend the CombinedContent to support run as well as interact_with_player, as we have not yet put in the properties that make it work.

The most complex interact_with_player is this one:

class DungeonControl(Content):
    def interact_with_player(self, dungeon) -> bool:
        self.state += 1
        if self.state >= len(self.resources):
            self.state = 0
        dungeon.publish('state_number', self.name, content=self, state=self.state)
        return self.enterable

That seems reasonable. The most complex run, however, is pretty gnarly:

class Animated(Content):
    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(*, content, state):
            try:
                cycling, state = self.cases[state]
            except KeyError:
                return
            self.set_state(cycling=cycling, state=state, dungeon=dungeon)
        dungeon.subscribe('state_number', self.name, control_changed)

That code also demands this method:

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

That’s a lot but it can be installed just defining a run_animated method and embedding all the other stuff in the class.

I think that if we go ahead with this, we’ll find that there are a lot of methods in the new class, but that most of them are private, in support of a handful of public pluggable ones. I’m inclined to go ahead and see what the resulting class looks like. It should permit us, when we are done, to remove all the other Content classes, including the abstract class, because there will just be one class, which we’ll most likely call Content.

Let’s go for Animated and see what it looks like. So long as we don’t break anything, we can make any changes to CombinedContent that we want to. It’s in use drawing decor but nothing else, so far.

First, bring the run method up to snuff:

    def test_run(self):
        touched = False
        def run_test(self, dungeon):
            nonlocal touched
            touched = True
        content = CombinedContent(name='item', resources=self.resources, scale=0.5)
        content.run_method = run_test
        content.run('fake')
        assert touched is True

Fails, because the function isn’t wrapped. Fix up the class.

class CombinedContent:
    @property
    def run_method(self):
        return self._run_method
    @run_method.setter
    def run_method(self, func):
        if isinstance(func, types.FunctionType):
            func = types.MethodType(func, self)
        self._run_method = func

    def run(self, dungeon):
        self._run_method(dungeon)

Test runs. Commit.

I think I’d like to have a class method on CombinedContent, to create spikes, which we’ll use to test in the game. And maybe even write a test. I don’t want to but OK, don’t look at me like that, I’ll try.

    def test_spikes(self):
        self.pub_sub = PubSub()
        content = CombinedContent(name='item', resources=self.spike_resources, scale=0.5)
        content.run_method = content.run_animated
        content.run(self)
        assert content.state == 0
        self.publish('on_update', 'view', delta_time=1)
        assert content.state == 1

This passes, with some rigmarole about subscribe and publish added to the test class, and in CombinedContent, all moved over from AnimatedContent:

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

    def run_animated(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(*, content, state):
            try:
                cycling, state = self.cases[state]
            except KeyError:
                return
            self.set_state(cycling=cycling, state=state, dungeon=dungeon)
        dungeon.subscribe('state_number', self.name, control_changed)

I think I’d like to make all these subordinate methods private. I’m a bit reluctant to turn PyCharm loose on that but after a commit we’ll try it. Works, commit.

I think we’ll work toward a class method to make spikes and then using it in main. But the controls subscription expects a cases member, which we do not have.

We could just add it as yet another parameter to the constructor. That could lead to trouble. We could embed it in the run_animated as a constant, but that would limit the kind of animated object we could have. (This one is actually a ControlledAnimatedContent, I suppose.)

Maybe we should have some sort of catch-all dictionary to keep auxiliary values in.

I think what we’ll do is hammer it in and then see how to make it better when we see the harm we’ve done.

New test calling the class method and checking the cycling variable. This will do as a start:

    def test_spikes_class_method_and_cycling(self):
        self.pub_sub = PubSub()
        content = CombinedContent.spikes()
        content.run(self)
        assert content.cycling is True

Now we need the class method:

class CombinedContent(Content):
    @classmethod
    def spikes(cls, name='spikes'):
        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),
        }
        instance = cls(name, resources, 0.75)
        instance.run_method = instance.run_animated
        instance.cases = cases
        return instance

Passes. Now let’s see if we can turn cycling off as intended.

    def test_spikes_class_method_and_cycling(self):
        self.pub_sub = PubSub()
        content = CombinedContent.spikes()
        content.run(self)
        assert content.cycling is True
        self.publish('state_number', 'spikes', content=self, state=1)
        assert content.cycling is False

The test passes. Commit. And in main:

    spikes = CombinedContent.spikes('spikes')
    dungeon.place_content_at(Cell(31, 30), spikes)

And the spikes work and respond to the lever! Life is good! Commit: Spikes using CombinedContent.

Reflection

So, what do we think abut all this? The class is already over 90 lines, counting white space. The class method for spikes is long, and the others will be similar. We might want to move over to a factory kind of style for those, to keep the main class clean.

Another possibility, especially for this animated one, might be to plug an object into the CombinedContent object and forward interact and run to it. But that seems likely to leave us in much the same situation as we’re trying to fix, with a bunch of classes each with their own implementation of interact and run.

If that’s the best structure we can think of, I’m OK with using it, but I’d like to see something better than all those classes, and remain hopeful that this CombinedContent object will serve.

It’s possible that breaking things out into interact and run isn’t quite right. Perhaps there are more atomic elements that should be assembled to create behavior. But so far we haven’t much behavior, just the simple true/false returns for entry and really only one non-trivial run, although it is seriously non-trivial. It’s also by far the most complex run we presently have.

I think what we’ll do is complete the set, making either more class methods, or some factory objects, and covering all the capability of the existing Content hierarchy. Then we can replace that hierarchy at our leisure and look to see how the new thing can be improved.

There is a common pattern that we see here, which is that we repeatedly subscribe to some event and provide a function to deal with it. So far those functions have been in line. Possibly there is a convenient abstraction to be found, covering this sign-up pattern, although since the functions called are mostly all custom made, it’s not clear there will be added clarity or ease of creation.

We’ll want to do something to provide for auxiliary values like our cycling and state and cases, and while state might be common enough to include, the others seem unique to a single kind of content, and probably should be stuffed into a standard dictionary of miscellaneous information.

We’ll figure something out.

Summary

I took a small chance by not assuring myself that the scheme would support the complex situation of the ControlledAnimated case, but it wasn’t much of a risk because there was less than two hours invested in the idea, and it seemed likely to be possible if not lovely. And that was how it turned out. Either way, we’d learn something, and trying to prepare for something as complicated as we did this afternoon is more speculation than I care to do.

I feel that it’s going well. Nothing really special had to be done to make Animated work, and it is by far the most complicated thing we have so far. As I mentioned this morning, it’s really a ControlledAnimated, since it can be turned on and off by a separate object, typically the lever, but in principle anything that can publish state messages.

Coming up, the lever, the button, the appearing content, the regular receivable content … and I feel sure that those will all fit into the scheme. Whether we’ll like the result remains to be seen.

… I was thinking about a strategy object again and I wonder whether we could make the current thing forward to a separate object if we wanted it to. I don’t see why not. Might be an interesting way to offload complex behavior but retain the simple ones with the simple scheme. Don’t know.

It is possible that the whole scheme will become unsatisfactory and we might revert. Or refactor to something completely different. It’s all good. It’s what we do.

See you next time!