Let’s add a Lever to our content objects. Herein, I make a prediction. Will I be correct?

My prediction is that installing the Lever will lead to code improvement, though I do not say whether it’ll be before installation, during, or after, or any of the other four non-zero combinations thereof.

I often make the point that I think it’s best in Real Life™ to focus refactoring and code improvement where we’re actually working for “feature” reasons, rather than on code that could use a little improvement but isn’t being worked with. My reasoning is that we have limited time to improve code, and that we do best to improve the code that’s impacting our work, not code that isn’t really bothering anyone.

I do not always follow that advice, even though I have the greatest respect for the fellow who gave it, because one of the main points of my work here is to show how we can improve code in very small steps, quick hits, so that we don’t need to find vast tracts of time to make things better. So, often, I’ll work on something in my code that wasn’t really causing any trouble.

Today, however, I think we’re likely to see an example of how new features can provide an opportunity to improve the code around them. Will that happen? We’ll see. I don’t know, I’ve only gotten to right here in the article.

The feature …

We have textures for a lever that has four positions. The plan is to have a content object that “is” the lever, and to use the lever’s states to control some spikes, which we also have textures for. In the game, the spikes will cycle up and down visibly, and if Dot steps on them while they are cycling, they’ll do serious damage to her if they are up, and slight damage if down.

Elsewhere in the dungeon, there will be a lever. Its positions control the spikes’ cycling, with position 0: cycling; 1: up; 2; fully down; 3: cycling. (We might randomize the numbers as a later improvement.) When the spikes are up, they do serious damage. When fully down, no damage at all. It will be to Dot’s advantage to work out which position is best. Maybe there will be clues. Maybe she’ll have to go look. All to be decided.

The design …

Clearly this thing is rather like the Button, except that it has a different number of textures to display and thus a different number of states. I don’t recall whether Button is already using the number of textures it has to control how many states it has, or not.

Either way, there’s clearly common capability between Button and Lever, and my prediction is that we’ll see that and make some improvements to the code that will make it more general, simpler, or perhaps both.

Let’s find out.

The work …

We begin, as always, by ensuring that we are on a clean repo, and then we take a look at the code we’ll need to change, namely Content and in particular, Button:

class Button(Content):
    def __init__(self, name):
        resource1 = '/Users/ron/Desktop/DungeonTiles/png/objects/floor button/Button (1).png'
        resource2 = '/Users/ron/Desktop/DungeonTiles/png/objects/floor button/Button (2).png'
        self.name = name
        self.scale = 0.75
        self.resources = [resource1, resource2]
        self.state = 0

    def interact_with_player(self, dungeon):
        self.state += 1
        if self.state >= len(self.resources):
            self.state = 0
        dungeon.publish('state_number', self.name, self, self.state)

    def run(self, dungeon):
        self.state = 0

We see that Button uses the length of its resource list to decide how many states it has. That’s useful. I wonder why we’re not using modular arithmetic there? And how does the picture get changed? That’s probably in the DungeonView, since the picture is a view thing:

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

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

    def view_do(self, content_item, action):
        try:
            view = self.content_views[content_item]
            action(view)
        except KeyError:
            return

class ContentView:
    def set_state(self, state_number):
        self.sprite.set_texture(state_number)

So, it seems that if we somehow created a Button with four textures, it would do the right thing.

I think the most direct thing, and perhaps the thing we’d do if we were in a hurry, would be to copy and paste the Button code into a Lever class and go from there. Like this:

class Lever(Content):
    def __init__(self, name):
        resource1 = '/Users/ron/Desktop/DungeonTiles/png/objects/lever/1.png'
        resource2 = '/Users/ron/Desktop/DungeonTiles/png/objects/lever/2.png'
        resource3 = '/Users/ron/Desktop/DungeonTiles/png/objects/lever/3.png'
        resource4 = '/Users/ron/Desktop/DungeonTiles/png/objects/lever/4.png'
        self.name = name
        self.scale = 0.75
        self.resources = [resource1, resource2, resource3, resource4]
        self.state = 0

    def interact_with_player(self, dungeon):
        self.state += 1
        if self.state >= len(self.resources):
            self.state = 0
        dungeon.publish('state_number', self.name, self, self.state)

    def run(self, dungeon):
        self.state = 0

I just copied, pasted, and added the two resources. Oddly, PyCharm is accusing this code of duplicating the init on Button, despite the two additional resources. Anyway, let’s add a lever to main and check it out.

main.py
    ...  appearing = AppearingContent('a brilliant torch', my_resources + 'torch/1.png', 1)
    dungeon.place_content_at(Cell(36, 29), appearing)
    lever = Lever('spikes')
    dungeon.place_content_at(Cell(31, 30), lever)

And voila!

map showing lever next to yellow star

When Dot steps onto the lever, it moves to the next position, as intended. No surprise there.

Well, not too much surprise. She winds up on top of the lever. We might want to make things such that she can’t actually step onto it, just bump it and have her move refused. I’m not sure we have that feature yet. And there would be concerns, such as possibly placing a lever so as to block some place that Dot really needs to go. We’d want not to put one in a narrow path, for example. Anyway, just a consideration to write down.

So the lever works as advertised. We can commit: implemented Lever content item.

Having made it work, let’s see about making it right. There’s all that duplication between Button and Lever. I wonder if PyCharm has an idea what to do about it or if it’s just complaining. It’s just complaining, but it will show me the two classes and a diff between them.

Let’s look at Button, as it is shorter. The only differences are in the init:

class Button(Content):
    def __init__(self, name):
        resource1 = '/Users/ron/Desktop/DungeonTiles/png/objects/floor button/Button (1).png'
        resource2 = '/Users/ron/Desktop/DungeonTiles/png/objects/floor button/Button (2).png'
        self.name = name
        self.scale = 0.75
        self.resources = [resource1, resource2]
        self.state = 0

OK, so. To make these the same, I think we have to pass the resources into the constructor.

Let’s change the init signature to include a resources parameter:

class Button(Content):
    def __init__(self, name, resources):
        resource1 = '/Users/ron/Desktop/DungeonTiles/png/objects/floor button/Button (1).png'
        resource2 = '/Users/ron/Desktop/DungeonTiles/png/objects/floor button/Button (2).png'
        self.name = name
        self.scale = 0.75
        self.resources = [resource1, resource2]
        self.state = 0

This does no harm. We could commit just to prove it. Now I think we’d like to have a class method to pass in the resources:

class Button(Content):
    @classmethod
    def button(cls, name):
        resource1 = '/Users/ron/Desktop/DungeonTiles/png/objects/floor button/Button (1).png'
        resource2 = '/Users/ron/Desktop/DungeonTiles/png/objects/floor button/Button (2).png'
        resources = [resource1, resource2]
        return cls(name, resources)

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

This will break main. We fix it:

    dungeon.place_content_at(Cell(33, 28), Button.button('a secret key'))

I changed both buttons. Should work. Does. Commit: changed Button class to have class method constructor button.

Now, I hope we agree that a class method on Button to make a lever would be useful.

class Button:
    @classmethod
    def lever(cls, name):
        resource1 = '/Users/ron/Desktop/DungeonTiles/png/objects/lever/1.png'
        resource2 = '/Users/ron/Desktop/DungeonTiles/png/objects/lever/2.png'
        resource3 = '/Users/ron/Desktop/DungeonTiles/png/objects/lever/3.png'
        resource4 = '/Users/ron/Desktop/DungeonTiles/png/objects/lever/4.png'
        resources = [resource1, resource2, resource3, resource4]
        return cls(name, resources)

Now we change main again, to create the lever:

    lever = Button.lever('spikes')

And we test. And the lever is there. And we commit: Button class has lever class method

And we delete the Lever class. And we test. And we commit: Removed Lever class.

And we rename Button to … let’s see … How about Control? Is that too generic? DungeonControl. We’ll try that on for size.

And we test. And we commit: rename Button to DungeonControl.

I love it when a plan actually comes together …

So that actually worked out, didn’t it? We took the absolutely simplest possible path to a Lever, copying an object and editing it into submission by adding a few different file names. That was done and committed and people could run around kicking levers. Then we added a button class method to Button and used it. Then we added a lever class method to Button and used it. Since Button and Lever were the same except for the file names, that just worked. Then we removed the Lever class and gave Button a better name, DungeonControl.

That, my friends, is just about how you do that.

But wait …

Wasn’t there a faster way? Couldn’t we have done it in fewer steps? Couldn’t we have added the class method, then the other class method, and “just” wound up where we are?

Coulda woulda shoulda. Don’t should on me. Certainly we could have done that. I hadn’t decided to do class methods, but I had the idea in mind as a possibility. But …

If we had done that, the Lever wouldn’t have worked until the end of the morning, instead of at the very beginning. And the first changes wouldn’t have been as abjectly simple as just duplicating the class and smushing its file list.

Better the way we did it? Worse? Don’t know, don’t care. What I do care about is that every step was just about as simple as I could think of, and left things in good order, including our new feature, from beginning to end.

You get to choose how you work and how you do things. I would never suggest otherwise. And, if you see value to what happened here this morning, maybe you’ll give it a try sometimes. Don’t forget the cleanup part, though! Otherwise things get messy fast.

I’ll put the class below just in case you want to review it. Maybe think about what further improvements might be in order. I see some …

See you next time!



class DungeonControl(Content):
    @classmethod
    def button(cls, name):
        resource1 = '/Users/ron/Desktop/DungeonTiles/png/objects/floor button/Button (1).png'
        resource2 = '/Users/ron/Desktop/DungeonTiles/png/objects/floor button/Button (2).png'
        resources = [resource1, resource2]
        return cls(name, resources)

    @classmethod
    def lever(cls, name):
        resource1 = '/Users/ron/Desktop/DungeonTiles/png/objects/lever/1.png'
        resource2 = '/Users/ron/Desktop/DungeonTiles/png/objects/lever/2.png'
        resource3 = '/Users/ron/Desktop/DungeonTiles/png/objects/lever/3.png'
        resource4 = '/Users/ron/Desktop/DungeonTiles/png/objects/lever/4.png'
        resources = [resource1, resource2, resource3, resource4]
        return cls(name, resources)

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

    def interact_with_player(self, dungeon):
        self.state += 1
        if self.state >= len(self.resources):
            self.state = 0
        dungeon.publish('state_number', self.name, self, self.state)

    def run(self, dungeon):
        self.state = 0