Python Asteroids on GitHub

Sensible thoughts lead me to wild speculation about the future of this tiny program. Who knew? Then, I create something nice: a Transponder!

It all started, Doctor, when I was just waking up and was thinking about if statements, which led to thinking about procedural code. In my early years of programming, about all we had was IF and DO, so that I tend to reach for the conditional statement quite often. Probably too often. So I was thinking about the if statement in my Missile constructor and that it might be not a good thing.

That led me to thinking about Kent Beck’s classic Smalltalk Best Practice Patterns, which made me think that I should review that book and see what the patterns tell us about this Python program. And that led to thinking about Beck’s Four Rules of Simple Design, one of which is that the code should always express all the programmer’s ideas about the code.

And that led me to thinking about the central notion of how this “decentralized” design works, with all the objects able to subscribe to messages about other objects, and their collaboration somehow turning into the game of Asteroids. The common elements of that core idea are not expressed at all vividly in the code, not least because I do not see just how they could be. Be that as it may, they’re not well expressed.

Since I wasn’t fully awake during any of this, and since I do not have the code memorized, the ideas were all quite vague. But they led me here to the keyboard where now I am speculating:

Perhaps a focus on removing procedural code, a focus on expressing ideas, and a focus on better coding patterns … perhaps all that could result in a even more compact and interesting, yet more understandable design than the one we have now,

So I’m going to try it. I’ll even try to dig out the Smalltalk book mentioned above. I don’t think I have the Java version and I’m quite OK with that. Smalltalk is far more object-oriented than Java anyway.

And, of course, we’ll do whatever we do incrementally. That may make it harder to see the big change from today’s version to some future one, but it will, if we’re fortunate, afford us many opportunities to see small changes making things improve gradually.

Let us code …

We’ll begin with an if. The one in the Missile creation, to be specific:

class Missile(Flyer):
    @classmethod
    def from_saucer(cls, position, velocity):
        return cls(position, velocity, [0, 0, 0], lambda score: 0, False)

    @classmethod
    def from_ship(cls, position, velocity):
        return cls(position, velocity, u.ASTEROID_SCORE_LIST, lambda score: score, True)

    def __init__(self, position, velocity, missile_score_list, confirmation, from_ship):
        self.confirm_score = confirmation
        if from_ship:
            self.saucer_tally = 0
            self.ship_tally = 1
        else:
            self.saucer_tally = 1
            self.ship_tally = 0
        self.score_list = missile_score_list
        self._timer = Timer(u.MISSILE_LIFETIME)
        self._location = MovableLocation(position, velocity)

This if statement, let’s face it, is pretty obscure. Its purpose is to allow the ship to keep a count of how many ship missiles still exist, and the saucer to keep track of how many saucer missiles exist. So the ship does this when interacting with missiles:

class Ship(Flyer):
    def interact_with_missile(self, missile, fleets):
        self._missile_tally += missile.ship_tally
        self.explode_if_hit(missile, fleets)

Ship accumulates ship_tally values from all the missiles that are in flight. Only the ship missiles have that value set to 1, so only those missiles are counted.

Now my brother GeePaw Hill would rightly point out that we have a data type trying to exist here, ShipMissile vs SaucerMissile. Yabbut, creating a subclass of missile just for counting doesn’t seem quite the thing. What might we do that would be better than what we have here?

It’s always good to start with a bad idea. It’s all better from there.

Now we certainly could have Missile class keep track of the counts. But to do that would require us to ensure that when a missile is removed, it reports back to the Missile class that it is gone. Some kind of finalize operation. We could do that, but it’s rather a large concept and makes everything more complicated, or so it seems to me.

An interesting question comes to mind. Given a ship, or a saucer, are all the matching missiles in existence created by that instance or could they have been created by a prior instance of the ship / saucer? In practice, the delay before spawning a new ship or saucer is greater than the missile timeout, so it is true that the current instance is the creator of any matching missiles. But that’s not coded as part of the system: it’s an artifact of specific timing. So we would not do well, for example, to have the missile keep a link to its creator and use that somehow in the tally.

What would be nice would be if there was just one message sent by either Ship or Saucer to tally missiles that were created by instances of that class. Right now there are two properties ship_tally and saucer_tally. One would be better.

Somewhat better.

For that to happen, we’d have to pass a parameter in the message. That could be the instance who is asking, maybe something like this:

class Ship:
	self._missile_tally += missile.tally(self)

The tally would return one or zero depending on whether the missile was created by someone like this ship. Or we could pass a key, essentially a synonym for the type:

class Ship:
	self._missile_tally += missile.tally("ship")

I think I almost like that.

We could pass a function to be called back if the missile is “one of ours”:

class Ship:
	missile.callback("ship", self.increment_tally)

Even better. Let’s try it.

Let’s try that last one, just to see what it might look like. We’re green and with no pending commits, let’s just spike something in.

class Ship(Flyer):
    def interact_with_missile(self, missile, fleets):
        missile.callback("ship", self.increment_tally)
        self.explode_if_hit(missile, fleets)

    def increment_tally(self):
        self._missile_tally += 1

This of course breaks many things, since there’s no callback method. Spiking:

    def callback(self, sender, function):
        if sender == "ship" and self.ship_tally == 1:
            function()

We’re green. This works. What we could do is store “ship” or “saucer” in missile._creator, like this:

    @classmethod
    def from_saucer(cls, position, velocity):
        return cls("saucer", position, velocity, [0, 0, 0], lambda score: 0, False)

    @classmethod
    def from_ship(cls, position, velocity):
        return cls("ship", position, velocity, u.ASTEROID_SCORE_LIST, lambda score: score, True)

    def __init__(self, creator, position, velocity, missile_score_list, confirmation, from_ship):
        self._creator = creator
        self.confirm_score = confirmation
        if from_ship:
            self.saucer_tally = 0
            self.ship_tally = 1
        else:
            self.saucer_tally = 1
            self.ship_tally = 0
        self.score_list = missile_score_list
        self._timer = Timer(u.MISSILE_LIFETIME)
        self._location = MovableLocation(position, velocity)

    def callback(self, sender, function):
        if sender == self._creator:
            function()

Yes, GeePaw, this is still a type check. But I still don’t think a separate class for Missile is worth it, and I’m not quite willing to lock in on remembering the specific creating instance.

Let’s do a tiny new object.

class KeyedCallback:
    def __init__(self, key):
        self._key = key

    def callback(self, key, function):
        if key == self._key:
            function()

Now lets use KeyedCallback in Missile:

    @classmethod
    def from_saucer(cls, position, velocity):
        return cls(KeyedCallback("saucer"), position, velocity, [0, 0, 0], lambda score: 0, False)

    @classmethod
    def from_ship(cls, position, velocity):
        return cls(KeyedCallback("ship"), position, velocity, u.ASTEROID_SCORE_LIST, lambda score: score, True)

    def __init__(self, callback, position, velocity, missile_score_list, confirmation, from_ship):
        self._callback = callback
        self.confirm_score = confirmation
        self.score_list = missile_score_list
        self._timer = Timer(u.MISSILE_LIFETIME)
        self._location = MovableLocation(position, velocity)

    def callback(self, sender, function):
        self._callback.callback(sender, function)

This has broken some tests, because I removed the weird tally members and Saucer is not au courant1 with the callback.

class Saucer(Flyer):
    def interact_with_missile(self, missile, fleets):
        missile.callback("saucer", self.increment_tally)
        if missile.are_we_colliding(self.position, self._radius):
            fleets.append(Score(self.score_for_hitting(missile)))
            self.explode(fleets)

    def increment_tally(self):
        self.missile_tally += 1

So this is interesting, but it’s probably not ideal.

We have coupling between ship, saucer, and missile, in that missile knows to create KeyedCallbacks with keys “ship” and “saucer”, and those classes know to provide those keys. Maybe it’s not a KeyedCallback. Maybe it’s … a Transponder!

This might actually be a good idea …

What if, when a flyer creates a missile, it could install a “transponder”, given a key from the creator, and when the flyer senses a missile, it could trigger the transponder, providing the key again plus a function to call, and the transponder would execute the function if the key matched.

The missile creation would install the transponder, but the key would be provided by the creator.

We still have to be mindful of the possibility that the missile came from a previous generation of the creator, so we can’t just pass the function to execute when we create the transponder, since it might be bound to a previous-generation flyer. Besides, we might want to use the transponder feature for something else.

Let’s rename that class to Transponder and see about using it differently.

class Missile(Flyer):
    @classmethod
    def from_saucer(cls, transponder_key, position, velocity):
        return cls(transponder_key, position, velocity, [0, 0, 0], lambda score: 0, False)

    @classmethod
    def from_ship(cls, transponder_key, position, velocity):
        return cls(transponder_key, position, velocity, u.ASTEROID_SCORE_LIST, lambda score: score, True)

    def __init__(self, transponder_key, position, velocity, missile_score_list, confirmation, from_ship):
        self._transponder = Transponder(transponder_key)
        self.confirm_score = confirmation
        self.score_list = missile_score_list
        self._timer = Timer(u.MISSILE_LIFETIME)
        self._location = MovableLocation(position, velocity)

    def ping_transponder(self, transponder_key, function):
        self._transponder.ping(transponder_key, function)

class Saucer(Flyer):
    def interact_with_missile(self, missile, fleets):
        missile.ping_transponder("saucer", self.increment_tally)
        if missile.are_we_colliding(self.position, self._radius):
            fleets.append(Score(self.score_for_hitting(missile)))
            self.explode(fleets)

    def increment_tally(self):
        self.missile_tally += 1

class Ship(Flyer):
    def interact_with_missile(self, missile, fleets):
        missile.ping_transponder("ship", self.increment_tally)
        self.explode_if_hit(missile, fleets)

    def increment_tally(self):
        self._missile_tally += 1

class Transponder:
    def __init__(self, key):
        self._key = key

    def ping(self, key, function):
        if key == self._key:
            function()

I think I rather like this. Missiles are constructed with a Transponder, whose key is provided by the missile creator, typically a ship or saucer. The Missile doesn’t know the key: only the creator and the transponder know it. When we ping the transponder on a missile, we provide a key and a function to be executed. The missile passes the ping to the transponder, including the key and the function. If the transponder recognizes the key, it executes the function.

The missile no longer knows its type! It no longer has a conditional in its __init__! The Transponder does an if, but it isn’t a type check, it is a data check, checking the incoming key to see if it is a legitimate ping signal.

All the code is right there up above. Neat, sweet, reet, complete.

I am removing the “spike” label from this code and am going to commit it. It’s clean, better than what we have, and has created a nice tiny object representing a clear abstraction: a Transponder.

The insertion of the object went quite smoothly, and only broke tests when expected to do so. PyCharm refactorings allowed changing the calling sequence to be done without manual changes, fixing up the tests and everything.

It is often risky to commit code that was generated as a spike, an experiment. Some of my betters would roll this back and do it again as a matter of principle. I myself will not. The Transponder object only has a constructor and one method, and the only callers are using it to increment a counter. It’s all simple. We can take it all in in the 40-odd lines above.

Commit: Convert from missile tally to use of keyed Transponder to tally a flyer’s own missiles.

Summary

After beginning with some wild-eyed speculation about how things might go over the long term, with an eye to using better coding patterns and fewer procedural constructs, I took a look at just one troubling issue, the slightly clever scheme I had for counting whose missiles belonged to whom.

This was replaced by allowing each missile creator to provide a secret encoding key from their Captain Asteroids Secret Decoder Ring, which would be burned into the ROM of the missile’s transponder, allowing the creator to ping the transponder to get it to do things, but only if the same key was provided.

The new mechanism is admittedly far more general than we had, and certainly could be used for all kinds of things, since it can execute any method. Is it too general? Not in a complexity sense: it’s nearly trivial. It is a useful tiny object. Will we have other uses for the thing? I don’t know. I can imagine forcing some in, but so far this is the only case we have where we want to distinguish some instances of a class from other instances.

Is it still a type-check, GeePaw? Honestly, I don’t know and don’t care. I like what we have better than what we had, and I definitely like it better than a new subclass of Missile. What this is, I’d argue, is a tiny example of distinguishing objects by delegation rather than by subclassing. I wonder where that thought might lead us.

Looking forward, I propose to seek out other oddities in this code, looking for occasions to generalize by using simple objects, rather than by writing extensive code considering options. And I’ll look for my copy of that book.

Let me know what you think of Transponder. I rather like it, which may be suspicious2 all on its own.

See you next time!



  1. Is he going to start using his inferior French again? Let’s hope not. 

  2. “Murder your darlings”, you know. Learned that from Sir Arthur Quiller-Couch, about a nineteen rail shot to me.3 

  3. As long as we’re doing quotes, Faulkner: “Get it down. Take chances. It may be bad, but it’s the only way you can do anything really good.” That’s how we do things here.