Python Asteroids+Invaders on GitHub

Let’s find something to improve the program, but small enough to be suitable for an early Sunday morning. I think we did something right!

Here’s our list of needed features from yesterday, with yesterday’s work struck through:

  • Game ends suddenly. When your final gunner is hit, the game flips instantly to the attract mode, which is in fact the asteroids attract mode. You don’t even get a moment to see your final score.
  • Scary sound. The game is supposed to make a scary sort of thumping sound that goes faster and faster.
  • Multiple waves. When you manage to shoot down all the invaders, you’re supposed to get a new rack. Those new racks start a bit lower down each time, increasing your need to take them out quickly.
  • Damage at bottom. When the invaders near the bottom of the screen, they should damage the shields,

    and when they get to the level of the player, the player should be destroyed. Possibly it’s game over: I’ll have to consider the ancient scrolls.

  • Saucer too fast? Watching old recorded games, it looks to me as if our saucer is moving too fast.
  • Pause on rez. When a new player is rezzed, the invaders, if a column is right over the left side, will fire instantly, often killing the player before he (I) even realizes he’s back. There should be a short delay before firing begins.

I guess it makes sense to address the invaders hitting the player, since we did the invaders hitting shields yesterday.

Invader Hitting Player Ends Game

Note
I change my mind about this below, changing the rule to just destroying any player hit by an invader. Same effect, takes a bit longer is all.

I think that’ll be the rule, at least for now. Why? Because once they are down there, if we did rez another player, they’d hit it immediately anyway. Here’s how we did that:

class Invader(Spritely):
    def interact_with_roadfurniture(self, shield):
        if self.colliding(shield):
            shield.sprite.mash_from(self)

That message is forwarded down through the InvaderFleet and InvaderGroup. But here it seems a bit odd, because Invader makes the decision to mash the shield, where usually we’d allow the shield to make its own decision about that.

In a perfect world, we would have the shield interact_with_invader and deal with it there. However, we don’t actually have all the invaders in the interaction loop, because it would cause over 3000 additional interactions every 60th of a second and that just seems wrong. Still, we could send a sort of pseudo interact_with_invader message over. We do that in the case of a shot:

    def interact_with_group_and_playershot(self, shot, group, fleets):
        if self.colliding(shot):
            player.play_stereo("invaderkilled", self.x_fraction())
            shot.hit_invader(fleets)
            group.kill(self)
            fleets.append(InvaderScore(self._score))
            fleets.append(InvaderExplosion(self.position))

We send the shot hit_invader. Let’s do that here: it’s better to do similar things in similar ways. When we do the next one, that will make three. But there’s an issue:

    def interact_with_group_and_playershot(self, shot, group, fleets):
        if self.colliding(shot):
            player.play_stereo("invaderkilled", self.x_fraction())
            shot.hit_invader(fleets)
            group.kill(self)
            fleets.append(InvaderScore(self._score))
            fleets.append(InvaderExplosion(self.position))

    def interact_with_roadfurniture(self, shield):
        if self.colliding(shield):
            shield.hit_invader(self)

These two hit_invader calls do not have the same parameters. That’ll be confusing. Let’s have the new rule be that we send both self (the thing the other guy hit) and the fleets. We’ll have to pass that in, of course.

First, I roll back my little change above, leaving interact_with_roadfurniture alone. One thing at a time. Now change the signature of the existing call to hit_invader:

    def interact_with_group_and_playershot(self, shot, group, fleets):
        if self.colliding(shot):
            player.play_stereo("invaderkilled", self.x_fraction())
            shot.hit_invader(self, fleets)
            group.kill(self)
            fleets.append(InvaderScore(self._score))
            fleets.append(InvaderExplosion(self.position))

class PlayerShot(Spritely, InvadersFlyer):
    def hit_invader(self, _invader, fleets):
        fleets.remove(self)

Green. Commit: hit_invader method now takes invader and fleets parameters.

Now we’ll track back to senders of interact_with_roadfurniture and pass in fleets. Change signature should find everything. No, looking at the implementors, I’ll do it by hand:

class Invader(Spritely):
    def interact_with_roadfurniture(self, shield, fleets):
        if self.colliding(shield):
            shield.sprite.mash_from(self)

class InvaderFleets(InvadersFlyer):
    def interact_with_roadfurniture(self, shield, fleets):
        self.invader_group.interact_with_roadfurniture(shield, fleets)


class InvaderGroup:
    def interact_with_roadfurniture(self, shield, fleets):
        for invader in self.invaders.copy():
            invader.interact_with_roadfurniture(shield, fleets)

That’s all good. Commit: interact_with_roadfurniture now passes fleets.

And now, we’ll call back to the shield when we hit it:

    def interact_with_roadfurniture(self, shield, fleets):
        if self.colliding(shield):
            shield.hit_invader(self, fleets)

And the shield will damage itself:

class RoadFurniture(Spritely, InvadersFlyer):
    def hit_invader(self, invader, _fleets):
        self._sprite.mash_from(invader)

And we are good. Commit: Shield now handles damage in hit_invader.

Reflection

What have we just done? We’re here to add a new feature, the game ending when an invader hits a player. We haven’t been working on that … or have we? We noticed that the same kind of thing was done in two different ways in the class we’re about to deal with. When the invader hit a shot, it sent a message to the shot. When it hit a shield, it just damaged the shield from afar. We made those two cases be the same, sending a hit_invader message to each, and we changed the message signature to support both needs (and to better match what we generally do with these things).

Now we are about to actually deal with invader-player collisions, and we’re ready to do it the same way as the other two. Prior to the preparatory changes, there were two ways to do it, and if we chose one, the other case would look odd. Now they’ll all look and behave the same.

Let’s get to it.

class InvaderFleet(Spritely, InvadersFlyer):
    def interact_with_invaderplayer(self, player, fleets):
        self.invader_group.interact_with_invaderplayer(player,fleets)

class InvaderGroup:
    def interact_with_invaderplayer(self, player, fleets):
        for invader in self.invaders.copy():
            invader.interact_with_invaderplayer(player, fleets)

class Invader(Spritely):
    def interact_with_invaderplayer(self, player, fleets):
        if self.colliding(player):
            player.hit_invader(self, fleets)

class InvaderPlayer(Spritely, InvadersFlyer):
    def hit_by_something(self, fleets):
        frac = self.x_fraction()
        player.play_stereo("explosion", frac)
        fleets.append(PlayerExplosion(self.position))
        fleets.remove(self)

    def hit_invader(self, invader, fleets):
        self.hit_by_something(fleets)

At the last minute, I decided that the invaders will destroy a player but not end the game. Of course, once they are down there, they will quickly destroy the next player if there is one. But this way seems better.

Now, there is another issue, which is that possibly, the original game just ended if the invaders got below the shields, but until I research that, this is my product decision and I like it, at least today.

Let’s reflect:

Reflection

We began with some simple refactoring and although tests broke briefly during the changes, in fact everything worked again when all the renaming was done. Then our new feature went in quite simply. Just pop the method in at the top, call down, call down, call, die. Nothing to it.

But we have no test for this behavior. It was just so obvious to me what to do that I went ahead without a test other than in the game. Let’s see: I think a test is easy, because there’s one similar already there. Yes, we have this already:

    def test_invader_damages_shield(self):
        maker = BitmapMaker.instance()
        sprite = Sprite(maker.invaders)
        invader = Invader(1, 1, sprite)
        shield = RoadFurniture.shield(Vector2(242, 816))
        assert not invader.colliding(shield)
        invader.position = shield.position
        assert invader.colliding(shield)
        old_mask = shield.sprite.mask
        invader.interact_with_roadfurniture(shield, None)
        assert shield.sprite.mask is not old_mask

We can sort of follow that pattern:

    def test_invader_kills_player(self):
        maker = BitmapMaker.instance()
        sprite = Sprite(maker.invaders)
        invader = Invader(1, 1, sprite)
        player = InvaderPlayer()
        fleets = FakeFleets()
        invader.interact_with_invaderplayer(player, fleets)
        assert not fleets.removes
        invader.position = player.position
        invader.interact_with_invaderplayer(player, fleets)
        assert fleets.removes
        assert player in fleets.removes

Our FakeFleets object tells us that upon the second interaction, something was removed. We double check that it is in fact the Player instance. Our code works. But we knew that.

Commit: Invader-player collision kills player. Let us know if you want something different, like end game.

Summary

That went well, didn’t it? I think it paid off to prepare the area for the new feature, because now all three of these slightly odd things are done in the same fashion. We’re kind of stuck with the invaders being special, but we can at least ensure that when they do their special things, they do them in as consistent a way as we can manage.

Our needed features list now looks like this:

  • Game ends suddenly. When your final gunner is hit, the game flips instantly to the attract mode, which is in fact the asteroids attract mode. You don’t even get a moment to see your final score.
  • Scary sound. The game is supposed to make a scary sort of thumping sound that goes faster and faster.
  • Multiple waves. When you manage to shoot down all the invaders, you’re supposed to get a new rack. Those new racks start a bit lower down each time, increasing your need to take them out quickly.
  • Damage at bottom. When the invaders near the bottom of the screen, they should damage the shields, and when they get to the level of the player, the player should be destroyed.

    Possibly it’s game over: I’ll have to consider the ancient scrolls.

  • Saucer too fast? Watching old recorded games, it looks to me as if our saucer is moving too fast.
  • Pause on rez. When a new player is rezzed, the invaders, if a column is right over the left side, will fire instantly, often killing the player before he (I) even realizes he’s back. There should be a short delay before firing begins.

We have more little things to do. I may even do some of them offline. But you know, as soon as I try to do something when you’re not watching, that will turn out to be an interesting one. That’s why I let you watch almost every time I program, because it seems there’s always something to learn.

The bad news is, hundreds of articles. The good news is, almost no matter where you look, there’s something interesting going on.

What did we learn today? I think it was something that we did right. The handful of changes made to kill the player when the invader hits it could have been done, exactly as they were, right when we started. Instead, in reviewing the code before starting, as one does, we noticed two ways of doing two very similar things. Either way would have worked for our new feature. But instead of choosing one and just doing it for our new feature, we first made the other two work the same way, slightly more general than either of them had had before. And only then did we do our new one.

Had we just done the new thing, the resulting code would have a surprise in it, one of the things done differently from the other two. What’s up with that, people would have asked. Now there will be just a bit less confusion, because all the things that seem the same are done the same way.

I think we did something right, for once. Thanks for helping! See you next time!