One More ...
Hello, loves!
I think that the animated spikes are the last challenge for the new content scheme. Are we up to it? Let’s find out.
An interesting comparison came to mind, between the old scheme and the new one. In the old scheme, I was creating classes that were intended to support multiple dungeon objects with different appearance and even somewhat different behavior. Buttons and spikes were instances of DungeonControl class. Using that scheme, I was continually striving to create reusable code that could support more than one kind of dungeon item.
In the new scheme, there is only one very tiny class. And, so far, there isn’t the same drive to converge types, although there are two factory methods, decor and receivable, that are able to be reused.
I think that what we’re likely to find with the new scheme is that there is commonality in the factory methods. To the extent that there is, and to the extent that dealing with it seems profitable, we’ll probably extract some methods, and perhaps even find some helpful objects.
I’m finding that thinking about building new kinds of content in the new scheme is easier than in the old one. In the old one, I had to think hard about how to jam a new form into one of the existing classes, and when I set out to create a new class, I had to think hard about how to make it general enough, so that it didn’t seem wasteful to build a class for just one purpose.
In the new scheme, it all comes down to determining a few events to publish and subscribe, and configuring a few callbacks. Most of that work is the same as before, maybe a bit more intricate, but the pieces seem more independent, less like they all have to be set up just right.
I’m finding it easier. So even given that the inner workings of the new CombinedContent object are a bit deep in the bag of tricks, overall, I think it’s simpler. If that keeps up, we’ll see faster progress and fewer mistakes than we would likely have using the old scheme.
Is the above actually the case? I don’t know. I can’t know: I can only report what I do and what happens. But it feels easier and better.
Spikey Content
Let’s do the spikes, which in the old scheme are the single existing type of the Animated content. We know there will be some changes, since we use the control event to control the action rather than sniffing the state_number event, but it’ll still be much the same.
The spikes expect to get a control (old scheme: state_number) event with values 0-3. It maps those values to two properties of its own, cycling and its own state, which is zero for spikes down and one for spikes up. If cycling is true, the spikes animate, if not, they stick in the provided position. The result of all that is that for the four positions of the Lever, there is only one, cycling==False and state==0 that is safe for transit.
We’ll review the existing code, which is fairly extensive, so let’s divide it up for easier consumption:
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]
scale = 0.75
cases = {
0: (True, 0),
1: (False, 1),
2: (False, 0),
3: (True, 0),
}
return cls(name, resources, scale, cases)
def __init__(self, name, resources, scale, cases):
super().__init__(name, resources, scale)
self.cases = cases
self.cycling = True
self.time = 0
def set_state(self, cycling, state, dungeon):
self.cycling = cycling
self.state = state
dungeon.publish('state_number', 'view', content=self, state=self.state)
def interact_with_player(self, dungeon) -> bool:
return True
def run(self, dungeon):
self.initialize_cycling(dungeon)
self.subscribe_to_on_update(dungeon)
self.subscribe_to_controls(dungeon)
Not much to see here. We’ll see how the cases are used in a moment, but given the discussion above it should be fairly clear that we’ll look up the entry corresponding to the lever’s event and set cycling and state from that entry, probably using the set_state method.
interact just returns True for now: it will rain down inconvenience upon Dot in due time.
The work is done in the events, of course:
class Animated(Content):
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(*, pub_sub, 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(*, pub_sub, 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)
We see a new event here that has not shown up for a while: on_update. Arcade gives the view an on_update call on every cycle, including an elapsed time since last call, some small fraction of a second. Our spikes cycle once every second, so in update_cycling, if we’re cycling, we update the time and when it wraps around we increment our state and report the state so that we’ll be redrawn. We wrap the state around, using the number of resources we have available. If we had more pictures than two, we’d cycle through them all.
Then in subscribe_to_controls, we use the control state to look up one of our cases and if we get one, we apply it. If we get a control event with a value out of our range, we ignore it. In our new scheme, this subscribe will be to the ‘control’ event, not ‘state_number’.
Reflection
The Animated(Content) class has lots of methods. Our new scheme doesn’t provide for methods, which makes me suspect that we’ll have some duplicated code, and that may be error-prone. We’ll keep an eye out for that and see if there’s anything we can do.
Round Tuit
We need a new factory method, so I’ll write at least a foundational test in the TestContentFactory test class.
def test_spikes(self):
factory = ContentFactory()
spikes = factory.spikes(name='spikes')
assert len(spikes.resources) == 2
It’s not much but it gets me started. I’m going to do as much as I can, far more than this test demands. I am not a perfect person, and TDD is not a religion, it’s a technique, and hell and damnation I’m just goin’ for it.
This took a bit longer than I’m comfortable with. Here’s the factory method and it works in the game:
def spikes(self, *, name):
resource1 = '/Users/ron/Desktop/DungeonTiles/png/objects/trap/1.png'
resource2 = '/Users/ron/Desktop/DungeonTiles/png/objects/trap/2.png'
resources = [resource1, resource2]
scale = 0.75
cases = {
0: (True, 0),
1: (False, 1),
2: (False, 0),
3: (True, 0),
}
info = SimpleNamespace(cycling=True, cases=cases, time=0)
def cycle(self, pub_sub, delta_time):
if not self.info.cycling: return
self.info.time += delta_time
if self.info.time >= 1:
self.info.time = 0
self.state = (self.state+1)%len(self.resources)
pub_sub.publish('state_number', self.name, content=self, state=self.state)
cycle_sub = Subscription(event='on_update', caller_id='', callback=cycle)
def control(self, *, pub_sub, state):
try:
self.info.cycling, self.state = self.info.cases[state]
pub_sub.publish('state_number', self.name, content=self, state=self.state)
except KeyError:
return
control_sub = Subscription(event='control', caller_id=name, callback=control)
return CombinedContent(name=name, resources=resources, scale=scale,
info=info, subs=[cycle_sub, control_sub],
)
My problems were mostly caused by leaving out the if not self.info.cycling:, which results in the spikes not responding to the control. Naturally I assumed that I wasn’t responding to the right control message or that it wasn’t being sent, etc etc, until I finally noticed the missing if.
Smack forehead, carry on.
For what it’s worth, this version of spikes is about 35 lines and the old style one is about 55. This is a marvelous moment to stop. Commit: spikes content factory working. No old style content in main.
Reflection
I think the new scheme is now officially working, as it is supporting all the objects in the dungeon created by main.
All the remaining accesses to the old Content hierarchy are tests. Most of those we can just drop, but I think there are a few cases where the objects are being used to exercise views and such. So we’ll want to go over them individually, fixing up those that still have value.
The tests for the new content are very weak. By their nature, the trivial ones need little or no testing, while the complex ones like the lever and spikes seem to require a rather messy story test. I think we’ll want to visit those and see what we can do about reasonable testing. Since they are tossing states and events back and forth, they deserve better testing than I’ve provided so far.
Speculation
As I look at an object like spikes, I get a sense of a high-level design that is done but not expressed. Something like this:
The
spikescontent has two states, up and down, and two modes, cycling or not cycling. It listens for a four-state ‘control’ event whose name is the same as its own, and upon receiving that event, sets ‘cycling’ and it’s own ‘state’ according to a lookup table. It then signals its own state to update the view.
The
spikesalso listen for the ‘on_update’ event. If cycling, it switches state every second.
Now, we can find all that information in the spikes factory method, but it is not as explicit as we might wish. I wonder whether we can improve the factory to help make that overall design more visible.
Summary
I am pleased at approximately the +5 level, called informally Pretty Darn Good. This new design seems quite decent. Simple things are simple, complicated things are still pretty straightforward, because everything falls into the same few slots: info, interaction, and subscriptions. I think we can make things more smooth and probably more expressive, but CombinedContent is doing what I had hoped.
Coming up, we’ll strip out the old objects, eliminating about 150 lines of code, we’ll improve some tests, and then think of something else weird to do. There are some interesting problems to solve, such as doors, narrow passages, and so on.
See you then!