Let’s give the spikes the ability to respond differently to control state, rather than the current fixed arrangement. Artisanal hand-crafted code, of course.

Hello, loves. I think we can rig up a small table to give some flexibility in how the spikes respond to the control messages. For now, we’ll just allow for a different mapping between the lever’s states and the spikes cycling and state.

Here’s the relevant code now:

class Animated(Content):
    ...
    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)

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

Let’s begin by adding a parameter to set_state, cycling. This will break our test:

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

The reason, of course, is that none of our callers have set cycling, so we’ll do that and use it everywhere, like this:

    def control_changed(control, state_number):
        match state_number:
            case 0 | 3:
                self.set_state(True, 0, dungeon)
            case 1:
                self.set_state(False, 1, dungeon)
            case 2:
                self.set_state(False, 0, dungeon)

Tests are green. Commit: refactoring toward pluggable behavior.

Now let’s call set_state just once in the above code, setting the values inside the cases and doing the set at the end. I’ll extract variables from each of those lines first, just because I like to use machine refactoring whenever I can.

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

I’m not really sure this is going to be much better than just doing it, but trying to find a way to do everything by machine refactoring pays off in skill, even when it’s weird. Move the common line down.

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

Extract the match code:

        def control_changed(control, state_number):
            cycling, state = determine_values(state_number)
            self.set_state(cycling, state, dungeon)

        def determine_values(state_number):
            match state_number:
                case 0 | 3:
                    cycling = True
                    state = 0
                case 1:
                    cycling = False
                    state = 1
                case 2:
                    cycling = False
                    state = 0
            return cycling, state

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

PyCharm does not understand that I really want that to be a method. Not to worry. Let’s change the function to return a table of values. I wonder if PyCharm can undo that | case for me. It can’t. Duplicate, edit, reorder:

    def determine_values(state_number):
        match state_number:
            case 0:
                cycling = True
                state = 0
            case 1:
                cycling = False
                state = 1
            case 2:
                cycling = False
                state = 0
            case 3:
                cycling = True
                state = 0
        return cycling, state

This is getting boring. Build the darn dictionary:

        def control_changed(control, state_number):
            cycling, state = determine_values(state_number)
            self.set_state(cycling, state, dungeon)

        def determine_values(state_number):
            cases = {
                0: (True, 0),
                1: (False, 1),
                2: (False, 0),
                3: (True, 0),
            }
            return cases[state_number]

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

Convert determine to a method by moving out outward and giving it a self parm:

    def determine_values(self, state_number):
        cases = {
            0: (True, 0),
            1: (False, 1),
            2: (False, 0),
            3: (True, 0),
        }
        return cases[state_number]

Make cases an instance variable initialized here:

    def determine_values(self, state_number):
        self.cases = {
            0: (True, 0),
            1: (False, 1),
            2: (False, 0),
            3: (True, 0),
        }
        return self.cases[state_number]

Move cases init to __init__:

    def __init__(self, name, resources):
        self.cases = {
            0: (True, 0),
            1: (False, 1),
            2: (False, 0),
            3: (True, 0),
        }
        self.resources = resources
        self.name = name
        self.cycling = True
        self.time = 0
        self.scale = 0.75
        self.state = 0

    def determine_values(self, state_number):
        return self.cases[state_number]

The tests are working on every change, by the way. We could be committing but I am still open to possibly resetting. Won’t happen though.

Move the cases to be a parameter in init.

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
        ...

Commit: spikes class method determines state settings in response to controls.

Let’s see what we have that needs a bit of improvement:

    def determine_values(self, state_number):
        return self.cases[state_number]

    def run:
        ...
        def control_changed(control, state_number):
            cycling, state = self.determine_values(state_number)
            self.set_state(cycling, state, dungeon)

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

Let’s inline the determine:

        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)

Commit that: tidying.

Let’s reflect and sum up.

Reflection

I think I “could” have done that faster by just creating the table and plugging it in, perhaps even starting with the spikes class method. Still, this way was mostly machine refactoring, though sadly PyCharm is missing a few capabilities.

Possibly, if I were willing to turn on some “AI” (ptui!) some agent might conceivably have done something similar to what was needed. If a person wanted that, a person should go somewhere else, because I am all about artisanal hand-crafted code. But I do think I could have done it in fewer steps, or, more accurately, I see now how I might have done it in fewer steps. I’m not sure I could do it in fewer safe steps, and this was pretty safe.

The code we have is not bullet-proof. In particular, if a state were to show up that is not in the table, we’d get a crash. Generally speaking, I prefer to test and code such that things like that can’t occur, but since these tables will be being provided more or less randomly, I’ve made a note to bullet-proof this code a bit.

Presumably what we will do when we need a lever with a different mapping is to provide a parameter on spikes, but as yet we don’t need that. I did want to put the table in while it was fresh in my mind, but in principle it could have waited as well.

Summary

A little progress, the sort of thing one does in the afternoon when a little coding seems appealing.

See you next time!