Python Asteroids+Invaders on GitHub

Despite a lot of musing about the Situation, I’ve not made enough progress to spike anything. What shall we do instead? A tiny improvement?

The basic Situation idea is that there’d be this collection of Situations, each one describing some bit of system state like “invader shot hits player”, and containing a list of things to do if that Situation arises. I’ve been thinking about it in my copious free time, and so far I don’t quite see how to make something of it. I’ve mostly been thinking in terms of the current game design, because the idea would be to figure out this better design and refactor to it. But it’s just not coming to me. I might try reducing the constraints and, as a different kind of exercise, see what I could do with the idea without limiting myself to things the current framework can readily do. If I get a decent idea, you can be sure I’ll write it up.

But for now, it’s time to move on. And the question is … where to move to? There’s not much missing from the game, no real unsolved problems. We have more than proven the original point with Invaders, which was that we could implement something other than Asteroids on the same framework. What’s left? Offhand, with a little thought:

  • We do not play the “beat” sound that was unique to Invaders;
  • We only have one player, and the original game had two;
  • We are not displaying the mystery score when you hit the saucer;
  • We do not support high score, and never have;
  • There’s almost certainly refactoring practice to be had;
  • It might be fun to make the RobotPlayer smarter, but it’s already better than I am;
  • Is there another explosion that doesn’t use our new explosion logic?

On that last one, there is the PlayerShot’s explosion: it explodes at the top of the screen:

class PlayerShot(SpritelyMixin, InvadersFlyer):
    def explode(self, fleets):
        fleets.append(GenericExplosion.shot_explosion(self.position, 0.125))
        fleets.remove(self)

Our Exploder object handles three other cases:

class Exploder():
    @classmethod
    def explode_invader(cls, position, fleets):
        invader_explosion_sound = "invaderkilled"
        explosion = GenericExplosion.invader_explosion(position, 0.125)
        cls.explode(position, invader_explosion_sound, explosion, fleets)

    @classmethod
    def explode_player(cls, position, fleets):
        player_explosion_sound = "explosion"
        explosion = GenericExplosion.player_explosion(position, 1.0)
        cls.explode(position, player_explosion_sound, explosion, fleets)

    @classmethod
    def explode_saucer(cls, position, fleets):
        saucer_explosion_sound = "ufo_highpitch"
        explosion = GenericExplosion.saucer_explosion(position, 0.5)
        cls.explode(position, saucer_explosion_sound, explosion, fleets)

    @classmethod
    def explode(cls, position, sound, explosion, fleets):
        frac = u.screen_fraction(position)
        player.play_stereo(sound, frac)
        fleets.append(explosion)

Oh, I think there is another explosion, player shot vs invader shot. Let’s see … no, that just does the same explode method. The shot exploding makes no sound. We’d have to accommodate that.

The question is … should we? As things stand now, other than tests, there are two classes using GenericExplosion, namely Exploder and PlayerShot. This is the only explosion that is not created by Exploder, and the only use of GenericExplosion outside of Exploder.

The change to the PlayerShot code is simple enough. Let’s type and see where we come out.

    def explode(self, fleets):
        Exploder.explode_player_shot(self.position, fleets)
        fleets.remove(self)

There is, of course, no such method on Exploder, so:

    @classmethod
    def explode_player_shot(cls, position, fleets):
        shot_sound = ""
        shot_explosion = GenericExplosion.shot_explosion(position, 0.125)
        cls.explode(position, shot_sound, shot_explosion, fleets)

It turns out that the player just ignores sounds that it does not know, so this actually works. It is not an accident that the player works that way. Here’s our code:

class Sounds:
    def play_if_possible(self, name, multi_channel):
        if sound := self.catalog.get(name):
            sound_is_not_playing = sound.get_num_channels() == 0
            a_channel_is_available = multi_channel or sound_is_not_playing
            if a_channel_is_available:
                self.channels[name] = sound.play()

We don’t play a sound that we can’t find in our catalog.

So that was good, we just followed our nose and now we can commit: PlayerShot explosion uses Exploder. All explosions now use it.

OK. We did some thinking, some coding, some writing. We’re an hour in. My computer wants to update. Let’s sum up.

Summary

As you can see above, it was about a half-dozen lines of typing to bring PlayerShot under the Exploder umbrella, which got all the Invaders explosions using Exploder. This is a tiny improvement, just a half-dozen lines. But I would argue that the net improvement isn’t tiny. We have isolated all explosions to one class, and isolated the use of the GenericExplosion to that class. We took the program from having more than one way to do a thing to having just one way. That’s a very useful effect from a very small change.

But … when would we do this, and when not?

If we had happened upon the PlayerShot for some reason and noticed the opportunity, I think we’d be wise to take it. If, while building Exploder, we had looked for and noticed PlayerShot, doing it then also would have made sense.

As for seeking it out as I did this morning, unless the team or some pair is quite idle, it might not make sense to do that. We may have more important work to do, and in general I would leave refactoring of a given object to an occasion when we need to change that object anyway.

But … these small changes make the code more consistent, and as such, it becomes easier to take in and to understand. It removes details of implementation from code that doesn’t need to think about details.

I believe that continually taking opportunities like this pays off. If I were again condemned to write software for money, I would work in this fashion. It helps me when I add new features or change older ones, and it gives me a little pop of joy when I make something better.

Can you find little pops of joy in your work? I hope so!

See you next time!