Adjustable Spikes
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!