Python Asteroids+Invaders on GitHub

I have the facts. Let’s draw four shields. Then shoot them a bit.

Well, I have some facts. The old code comments say that the shields are 22 pixels across, and that they are placed 23 pixels apart. We are at scale 4, so we have, if they are centered:

spacing + 4*22 + 4*23 + 4*22 + 4*23 + 4*22 + 4*23 + 4*22 + spacing ==
spacing + 88 + 92 + 88 + 92 + 88 + 92 + 88 + spacing ==
spacing + 628 + spacing == 1024
2*spacing == 1024 - 628 == 396
spacing == 198

The step between the origins is 88 + 92 or 180. And we measure from the center not the upper left, so we need to add half the width of the shield or 44. I don’t know the y coordinate yet: that will take more exploration. I’ll estimate it and eyeball it and wind up with this:

coin.python

def invaders(fleets):
    fleets.clear()
    left_bumper = 64
    fleets.append(Bumper(left_bumper, -1))
    fleets.append(Bumper(960, +1))
    fleets.append(TopBumper())
    fleets.append(InvaderFleet())
    fleets.append(InvaderPlayer())
    fleets.append(ShotController())
    half_width = 88 / 2
    spacing = 198
    step = 180
    for i in range(4):
        place = Vector2(half_width + spacing + i*step, 800-16)
        fleets.append(Shield(place))

shields just a bit above player

That looks good for now.

No tests involved, but many magic numbers. We’ll settle, for now. Commit: game displays four shields.

What’s our next story?

  1. Get one shield on the screen;
  2. Get all four on the screen;
  3. Shots ignore shields;
  4. Shots die upon hitting shields;
  5. Shots do simple damage;
  6. Shots do fancy damage;

We got #3 for free. My cunning plan is working out. Surely we are ahead of schedule now. Let’s add the interaction with shield and invader shot. Lets test-drive it if we can.

I notice a bug! Invader shots do not die when they hit the player. They continue on down to the edge. We’ll test-drive that one too. I start with this:

    def test_dies_on_shield(self):
        fleets = Fleets()
        fi = FI(fleets)
        shield = Shield(Vector2(100, 100))
        maker = BitmapMaker.instance()
        shot = InvaderShot(Vector2(100, 100), maker.rollers)
        assert shot.colliding(shield)

That much passes. And then:

    def test_dies_on_shield(self):
        fleets = Fleets()
        fi = FI(fleets)
        shield = Shield(Vector2(100, 100))
        maker = BitmapMaker.instance()
        shot = InvaderShot(Vector2(100, 100), maker.rollers)
        assert shot.colliding(shield)
        fleets.append(shot)
        assert fi.invader_shots
        shot.interact_with_shield(shield, fleets)
        assert not fi.invader_shots

Failing as expected on the not. We need a better interact_with_shield:

    def interact_with_shield(self, shield, fleets):
        if self.colliding(shield):
            self.die(fleets)

And we’re green. Make the same kind of test for InvaderPlayer:

    def test_dies_on_player(self):
        fleets = Fleets()
        fi = FI(fleets)
        player = InvaderPlayer()
        player.position = Vector2(100, 100)
        maker = BitmapMaker.instance()
        shot = InvaderShot(Vector2(100, 100), maker.rollers)
        assert shot.colliding(player)
        fleets.append(shot)
        assert fi.invader_shots
        shot.interact_with_invaderplayer(player, fleets)
        assert not fi.invader_shots

Fails as did the other. Fix as we did the other:

    def interact_with_invaderplayer(self, player, fleets):
        if self.colliding(player):
            self.die(fleets)

We now have three methods with those two lines in them. Let’s extract before we commit.

    def interact_with_invaderplayer(self, player, fleets):
        self.die_on_collision(player, fleets)

    def die_on_collision(self, flyer, fleets):
        if self.colliding(flyer):
            self.die(fleets)

    def interact_with_playershot(self, shot, fleets):
        self.die_on_collision(shot, fleets)

    def interact_with_shield(self, shield, fleets):
        self.die_on_collision(shield, fleets)

That’s nice. Commit: Invader shot dies on hitting player or shield. (I confess that I did run the game to see it happen.)

Another story—well, half story—bites the dust. I split the shots stories into Invader ones and Player ones:

  1. Get one shield on the screen;
  2. Get all four on the screen;
  3. Shots ignore shields;
  4. InvaderShots die upon hitting shields;
  5. PlayerShots die upon hitting shields
  6. InvaderShots do simple damage;
  7. PlayerShots do simple damage;
  8. InvaderShots do fancy damage;
  9. PlayerShots do fancy damage;

The damage ones are a bit tricky. I want to check out some pygame features and think about it before I start, so we’ll wrap this up for this afternoon. We’ll be more fresh tomorrow.

Summary

Getting four shields on the screen involved some fence-post arithmetic and it wasn’t too hard. I need to look into the Y positions for player and shields, however. They are just approximated for now.

The shot collision logic was easy on the shot side. On the Shield side, it’s the damage that will be tricky, as we’ll have to erase some pixels from the shield and make it look good. And, we’ll have to consider the holes in the shield when assessing the next possible collision. Given how our Collider works, that “just” means we’ll have to update the shield mask, I think.

We’ll find out more next time! See you then!