Python Asteroids+Invaders on GitHub

Let’s take the next steps to, um taking the next steps.

So, right. Our mission is to extend our nascent Space Invaders game so that the invaders can move back and forth and downward. When we’re done, we are supposed to move just one invader on each call to update, so that they sort of ripple across and down the screen. But we’ll begin by moving them all at once.

The “mix” will consist of our InvaderFleet, which contains 55 invaders to start with, and two Bumpers, one on each side. Let’s start with that setup. In coin:

coin.py
def invaders(fleets):
    fleets.clear()
    fleets.append(Bumper(16))
    fleets.append(Bumper(u.SCREEN_SIZE - 16))
    fleets.append(InvaderFleet())

A quick check just to see what explodes. Nothing does, so that’s good. Let’s see if I can devise a test for update.

The call to update comes every 60th of a second, and it’s passed delta_time and the current fleets instance. Our screen is 1024 wide and our “invaders” are currently spaced 64 pixels apart. In the original game, they are 16 pixels wide, abutting, and that’s on an approximately 256-bit pixel screen, so I think that if 64 is 4 times 16, that spacing is about right. So, let’s see, we’ll have 320 pixels of motion all the way to the edge, minus the 32 I’m allowing right now, so about 288 pixels to move one side to the other.

The videos of the game seem to take about 10 seconds side to side, so let’s say that’s 20 pixels per second. (We’ll have to set all these values into our universal constants in due time, of course.)

Let’s do a simple test just to get the ball rolling:

    def test_fleet_motion(self):
        fleet = InvaderFleet()
        assert fleet.step == Vector2(30, 0)
        fleet.at_edge()
        fleet.end_interactions(None)
        assert fleet.step == (Vector2(-30, 0))

So a member, step, that’s either (30, 0) or (-30, 0), subject to reversal.

class InvaderFleet(Flyer):
    def __init__(self):
        self.invaders = [Invader(x//5, x % 5) for x in range(55)]
        self.origin = Vector2(u.SCREEN_SIZE / 2 - 5*64, 512)
        self.step = Vector2(30, 0)
        self.reverse = False
        self.update(0, None)


    def end_interactions(self, fleets):
        if self.reverse:
            self.reverse = False
            self.step = -self.step

Test is green. Commit: InvaderFleet has step and reverses it after at_edge is called.

Now test that we move an invader on update. I’ll just test the starting one so that when we do the ripple, this test will continue to work.

    def test_fleet_motion(self):
        fleet = InvaderFleet()
        assert fleet.step == Vector2(30, 0)
        pos = fleet.invaders[0].position
        fleet.update(1.0, None)
        new_pos = fleet.invaders[0].position
        assert new_pos - pos == fleet.step
        fleet.at_edge()
        fleet.end_interactions(None)
        assert fleet.step == (Vector2(-30, 0))

That’s failing, first because the invader doesn’t know “position”.

    @property
    def position(self):
        return Vector2(self.rect.center)

Still failing, I hope for a zero vector from the subtract.

Expected :<Vector2(30, 0)>
Actual   :<Vector2(0, 0)>

Perfect! Now fix update to move the invaders:

    def update(self, delta_time, _fleets):
        self.origin += self.step*delta_time
        for invader in self.invaders:
            invader.move_relative(self.origin)

I am not loving the fact that I set delta_time to 1.0, since I could have forgotten to multiply it in. Even worse, when I remove the multiply, the test fails. That rather surprises me.

Ah, it’s a different test. The error is:

    def test_fleet_origin(self):
        fleet = InvaderFleet()
>       assert fleet.origin == Vector2(u.SCREEN_SIZE / 2 - 5*64, 512)
E       assert <Vector2(222, 512)> == <Vector2(192, 512)>
E         Full diff:
E         - <Vector2(192, 512)>
E         ?          ^^
E         + <Vector2(222, 512)>
E         ?          ^^

Why would delta_time affect that? Ah! We use update with a delta_time of zero to initialize our invaders positions. Without the multiply, we use 30.

class InvaderFleet(Flyer):
    def __init__(self):
        self.invaders = [Invader(x//5, x % 5) for x in range(55)]
        self.origin = Vector2(u.SCREEN_SIZE / 2 - 5*64, 512)
        self.step = Vector2(30, 0)
        self.reverse = False
        self.update(0, None)

So the delta_time is necessary and tested.

    def update(self, delta_time, _fleets):
        self.origin += self.step*delta_time
        for invader in self.invaders:
            invader.move_relative(self.origin)

Super. I think the invaders will move on the screen now. They do, but they move right off.

Ah, we aren’t handling the interact_with_bumper message in InvaderFleet. OK. I do that:

    def interact_with_bumper(self, bumper, _fleets):
        for invader in self.invaders:
            invader.interact_with_bumper(bumper, self)

But Bumper doesn’t implement interact_with! My ignored tests might have told me that.

class Bumper(Flyer):
    def __init__(self, x):
        self.rect = Rect(x, 0, x+1, u.SCREEN_SIZE)

    def interact_with_invaderfleet(self, invader, fleets):
        pass

    def interact_with(self, other, fleets):
        other.interact_with_bumper(self, fleets)

That ought to do it. No … we need interact_with_bumper … there are two bumpers.

class Bumper(Flyer):
    def __init__(self, x):
        self.rect = Rect(x, 0, x+1, u.SCREEN_SIZE)

    def interact_with_invaderfleet(self, invader, fleets):
        pass

    def interact_with_bumper(self, bumper, fleets):
        pass

    def interact_with(self, other, fleets):
        other.interact_with_bumper(self, fleets)

And we are green. And we have this:

red dots on yellow background drift from side to side, reversing near edges

Let’s commit: Invaders move back and forth, as a unit, bouncing off bumpers. And let’s sum up.

Summary

This is good progress. I’m sure the team will enjoy seeing the invader substitutes moving back and forth. What’s neat about it is that Bruce’s idea worked perfectly, as one would expect both because his ideas are good and because the decentralized design is just so nice for things like that.

We need to get rid of a lot of magic numbers, but I like that in the tests and even in the code, we kind of show our work in coming up with values.

I think our current Invader rectangle isn’t good, but we’ll deal with that as soon as we plug in the bitmaps. We may find it desirable to move the bumpers when we put in the real pictures, since they’ll collide with the blank space at the sides of the invaders: they have two blank columns on each side of their 16x8 basic layout. Easily tuned, I’m sure.

I think we’d be wise to create separate hierarchies for each game (Asteroids and Invaders, but who knows what we might do next), so that we can take advantage of the class methods in the upper interface, and so that we can fix up those two ignored tests that are checking for methods that our objects don’t really need. I think just making two subclasses of Flyer, AsteroidsFlyer and InvadersFlyer, with suitable abstract class methods, will do the job for the compiler, but the special inspection code in the tests probably needs reworking. We’ll see.

Definitely should be done, and probably rather tedious. I hope I have the gumption to do it.

Overall, it seems to be going well. Next time, maybe some refactoring, or maybe the ripple effect.

See you then!