Python Asteroids+Invaders on GitHub

The words “time capsule” popped into my head last night. Have I told you my favorite thing about this program’s design?

Of course I have. I’ll tell you again.

I love how the game we play “emerges” from the combination of the various behaviors of the many different objects that are in the mix. If we toss asteroids and a ship in, we get the game of Asteroids. If we toss a player and invader fleet in, we get the game of Space Invaders. The same thing works at a more detailed level. An invader, when it is hit by a player shot, removes itself from the fleet. It also tosses an InvaderExplosion into the mix. The explosion draws the explosion pattern, and it counts down an eighth of a second and then removes itself from the mix. Voila! Exploding invader.

In the Space Invaders game here chez Ron, we are at the stage where there is a fleet of invaders, and a player. When the player is hit, it does the player-exploding dance, but it is still active and it does not get destroyed. We just haven’t done that part of things yet. It’s coming up on time to do so, and TimeCapsule is my new idea for a good way to do what we need.

In the Asteroids game, there is a ShipMaker object in the mix. The ShipMaker owns two objects, a Timer and a ShipProvider. These are not Flyers: they do not go into the mix. They are just helper objects that ShipMaker uses. When it notices that there is not a ship in the mix, ShipMaker ticks down its timer, and when its timer expires, it goes through the process of deciding whether it can create a ship, and ultimately puts a ship into the mix or tosses in a GameOver object instead.

The upshot is that ShipMaker worries about whether there are ships, whether the time has elapsed, whether it is safe to create a ship, and whether there are ships to create, as well as then creating either a Ship or a GameOver. That’s rather a lot, and I think we can do better.

A “time capsule”, as all here surely know, is a container of some kind, containing artifacts or treasures, buried or otherwise hidden, intended to be opened later to the edification of the population at that future time. In short, you put stuff in it, and later, the stuff comes out.

My cunning plan is to build a tiny object, TimeCapsule, into which we can put some other object, such as an InvaderPlayer. After a discreet interval, the TimeCapsule opens and tosses its contents into the mix, and removes itself. Thus, a two-second TimeCapsule containing an InvaderPlayer would wait two seconds, toss in the player, and remove itself. And we get a nice two second delay between destroying one player and rezzing another.

Some questions arise. How does the TimeCapsule get in there? Is there a PlayerMaker object always in the mix? Does the player itself, as its last dying act, create the TimeCapsule? How is it decided whether the TimeCapsule should contain another Player, or a GameOver?

I do not know the answers to these questions. I was hoping that you did. No matter, we’ll figure it out.

“Stories”

I have just watched more videos of Space Invaders than anyone should have to, and it seems to me that when the player is destroyed, the invaders freeze on screen, and after a short interval of a second or two, a new player appears and the game starts up again. It appears that the player always appears at the far left.

Our player object handles exploding, though it does not stop responding to controls yet. So our first few steps might be something like this:

  1. When exploding, the player should not be able to move or fire.
  2. After exploding, the player should add a TimeCapsule containing a new ship, and
  3. should remove itself.
  4. The TimeCapsule should wait for some time—try one second— and then add a Player.

We should test-drive as much of this as we reasonably can. Let’s talk about that.

The Player is pretty simple and we are asking it to do something pretty simple. Surely we could just pop that in.

The TimeCapsule is simple. We can design it right here. It’s created with a Flyer and a time delay. It ticks down the tie delay and when it elapses, it puts the Flyer into the mix and removes itself. It interacts with nothing.

I swear I could type TimeCapsule in right now and get it right. Why test-drive it? I think my real answer is: because that is how I generally do best. I want to confirm my habit of doing what works best for me, even in instances where I could get away without it.

The phrase “get away without it” just flowed out of my fingers without really thinking. If that doesn’t tell me that not test-driving is “wrong” in my book, I’m just not paying attention. I truly know that tests are so helpful to me that they are always worth doing. I also know that more exercise would be good for me. I know a lot of things that would be better and yet I seem to be flawed. How interesting …

Right now, I think this class does not need test-driving. I’m going to do it anyway. Maybe we’ll see when I’m done that we didn’t need it. Even so, we’ll at least have an example of how to use it. Let’s find out.

class TestTimeCapsule:
    def test_can_create(self):
        thing = "thing"
        capsule = TimeCapsule(thing, 2)

And:

class TimeCapsule:
    def __init__(self, flyer, time):
        self.flyer = flyer
        self.time = time

That’s a little more code than the test requires, but I’m not a fanatic about that “never write a line of code that isn’t required by a failing test”. I do think that’s a good way to work, and definitely a good way to start with TDD, and, honestly, a good way to work all the time, but I am old and tired and set in my ways and I forgive myself these foibles. If I gave advice1, I’d advise you to forgive yourself as well.

With the requisite import, the test runs. Commit: Initial TimeCapsule and test.

OK, well, the next test wants to determine that the TC does not add its thing right away and does add it after a while2.

    def test_after_a_while(self):
        crocodile = PlayerShot((100, 100))
        a_while = 2
        capsule = TimeCapsule(crocodile, a_while)
        fleets = Fleets()
        fi = FI(Fleets)
        fleets.append(capsule)
        fleets.tick(1)
        assert not fi.player_shots
        fleets.tick(1)
        assert fi.player_shots
        assert fi.player_shots[0] == crocodile

These tests are always a bit long. It seems to be the price we pay for all the objects interacting as they do.

The test fails, telling me that TimeCapsule has no attribute tick. Sure doesn’t. It’s not even a Flyer. We have some work to do and some methods to add.

Should TimeCapsule be a Flyer, an AsteroidsFlyer, or an InvaderFlyer? I think it’ll suffice to make it a Flyer. Is Flyer supposed to be purely abstract? I see no reason to have that rule.

Making it inherit Flyer and allowing PyCharm to implement required methods, we get this:

class TimeCapsule(Flyer):
    def __init__(self, flyer, time):
        self.flyer = flyer
        self.time = time

    def interact_with(self, other, fleets):
        pass

    def draw(self, screen):
        pass

    def tick(self, delta_time, fleets):
        pass

And in tick:

    def tick(self, delta_time, fleets):
        self.time -= delta_time
        if self.time <= 0:
            fleets.remove(self)
            fleets.append(self.flyer)

I rather expected that to work. It didn’t. What have I missed?

The check is failing.

_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
tests/tools.py:60: in player_shots
    return self.select_class(PlayerShot)
tests/tools.py:31: in select_class
    return self.select(lambda a: isinstance(a, klass))
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <tests.tools.FleetsInspector object at 0x104f6dc90>
condition = <function FleetsInspector.select_class.<locals>.<lambda> at 0x104f4cfe0>

    def select(self, condition):
>       return self.fleets.select(condition)
E       TypeError: Fleets.select() missing 1 required positional argument: 'condition'

I finally see the problem:

        fleets = Fleets()
        fi = FI(Fleets)

Should be:

        fleets = Fleets()
        fi = FI(fleets)

Test is green. Commit: TimeCapsule adds flyer after time elapsed.

It appears that the only mistake I made was in the test, which is somewhat amusing. On the other hand, now I am quite confident in my tiny little TimeCapsule, and feel no need to test it in the game to find out if it works.

I do feel like testing it in the game to see if I like the delay. Let’s just change coin.py:

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(TimeCapsule(InvaderPlayer(), 2))  # <===
    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))

That, I will try. I know of one thing that needs changing: the fleet needs to stop when there is no player.

Ah. We get a run-time error. My use of Flyer as the superclass was a mistake. We need all the Invaders interactions, even if only as pass. Change it.

I set up all those methods as pass in TimeCapsule. But there is another issue, which is that one of my protocol checking tests has detected that there is a class, TimeCapsule, and no interact_with_timecapsule in InvadersFlyer.

I add it to the ignores:

    def check_class(self, test_class):
        subclasses = get_subclasses(test_class)
        ignores = ["BeginChecker", "EndChecker", "TimeCapsule"]
        ...

I think we need more robust declaration of things or perhaps a new interface for objects with less interaction than others have. It’s always a bit less simple than we’d like. We’d like just to create our new class and toss it in. Instead we often have to implement methods on other classes, and/or update this protocol checking test.

It’s good to have things needing improvement. Now let’s test in the game again.

I like the two-second delay. I do not like that the invaders start moving, but we’ll deal with that. And the player should start at far left, whatever that means.

We can commit: coin creates invaders game with timecapsule containing player.

Let’s reflect, review our story list, and revise it.

Reflection

This was almost as easy as it “should” have been, but if we were going to be adding objects a lot, we’d want them to be able to be added more independently than is currently feasible. And TimeCapsule is just full of methods it really doesn’t need: 11 interacts, all pass, plus draw, rect, mask, and interact_with.

I’m glad I wrote the tests. It was easy enough and by the time I had it working, my confidence in the trivial TimeCapsule was higher than if I had just coded it up.

Maybe we need another interface, NoninteractingFlyer or something, that defaults more of those methods. There is the risk, always, that those defaults will inject defects as we forget to un-default something. I think it would take some fairly deep thinking to define just how an ideal hierarchy might be created, and I have two issues with that. First, deep thinking, while it is fun, is error-prone, and second, complex hierarchies are also error-prone. We’ll hold off on that idea but surely will think about it.

Anyway, TimeCapsule is doing just what we expected. Let’s look at the “stories”, cross out the TimeCapsule one, and add in any new ones we have thought of:

  1. When exploding, the player should not be able to move or fire.
  2. After exploding, the player should add a TimeCapsule containing a new ship, and
  3. should remove itself.
  4. The TimeCapsule should wait for some time—try one second— and then add a Player.
  5. The fleet should freeze when there is no player.
  6. The player should start at far left.

Moving Right Along

Let’s do the player features, freezing while exploding and adding a TimeCapsule when finished. We need to review how InvaderPlayer works now, to decide how to test and code this.

class InvaderPlayer(InvadersFlyer):

    def interact_with_invadershot(self, shot, fleets):
        collider = Collider(self, shot)
        if collider.colliding():
            self.explode_time = 1

    def draw(self, screen):
        self.explode_time -= 1/60
        if self.explode_time > 0:
            player = self.players[random.randint(1,2)]
        else:
            player = self.player
        screen.blit(player, self.rect)

This is quite simple. Perhaps too procedural?

Right now, we just set the explode time and animate a bit while counting it down. When it has counted down, we need to remove this guy and add our new TimeCapsule.

I’m going to do this in draw, and I’m going to do it by hand. I hang my head in shame. I really should test this. And we do have some player tests. But I want something better than one of those long Fleets story tests. Let’s see if we can invent something a bit easier.

One notion comes to mind. Anything to avoid a test. Right now, exploding is a state in the InvaderPlayer, controlled by that explode_time counter. What if there was a new object, PlayerExplosion, that Player tossed in on collision, immediately removing itself, and what if PlayerExplosion ran for however much time and then removed itself, tossing in the TimeCapsule?

I like that. TimeCapsule is tested, so that part is mostly solved.

It would probably be easy enough to rig a Flyer to tell a test what it intends to remove and what it intends to add. It would add a little layer to the object, but that may be OK and we could avoid the long tests.

Let’s try a new Player test. I want to write this test:

    def test_collision_removes_self(self):

Hey! What if we passed in a fake fleets object that recorded what was added and removed? Let’s assume that. I’ll code some more test and then see what it needs:

    def test_collision_removes_self(self):
        fleets = FakeFleets()
        player = InvaderPlayer()
        player.hit_by_shot(fleets)
        assert player in fleets.removes
        added_tc = [tc for tc in fleets.appends if isinstance(tc, TimeCapsule)]
        assert added_tc

I’ll start by putting FakeFleets in the tools module. I won’t TDD it because this test will TDD it, as well as TDDing the new stuff in InvaderPlayer.

class FakeFleets():
    def __init__(self):
        self.appends = []
        self.removes = []

    def append(self, thing):
        self.appends.append(thing)

    def remove(self, thing):
        self.removes.append(thing)

Might be right. Import it and move on. Now there is no hit_by_shot method in InvaderPlayer. Let’s see where it should go:

class InvaderPlayer(InvadersFlyer):
    def interact_with_invadershot(self, shot, fleets):
        collider = Collider(self, shot)
        if collider.colliding():
            self.explode_time = 1

Extract that last line to hit_by_shot:

    def interact_with_invadershot(self, shot, fleets):
        collider = Collider(self, shot)
        if collider.colliding():
            self.hit_by_shot()

    def hit_by_shot(self):
        self.explode_time = 1

How’s the test feeling right about now?

>       player.hit_by_shot(fleets)
E       TypeError: InvaderPlayer.hit_by_shot() takes 1 positional argument but 2 were given

We need the fleets. Pass it:

    def interact_with_invadershot(self, shot, fleets):
        collider = Collider(self, shot)
        if collider.colliding():
            self.hit_by_shot(fleets)

    def hit_by_shot(self, fleets):
        self.explode_time = 1

Test now whines:

>       assert player in fleets.removes
E       assert <invader_player.InvaderPlayer object at 0x1052c7b50> in []
E        +  where [] = <tests.tools.FakeFleets object at 0x1052ed010>.removes

Right, we’re not removing ourselves.

    def hit_by_shot(self, fleets):
        self.explode_time = 1
        fleets.remove(self)
        added_tc = [tc for tc in fleets.appends if isinstance(tc, TimeCapsule)]
>       assert added_tc
E       assert []

Now we have no time capsule. Add it.

    def hit_by_shot(self, fleets):
        self.explode_time = 1
        fleets.remove(self)
        fleets.append(TimeCapsule(InvaderPlayer(), 2))

The tests all pass. If we run the game now, we will see the player disappear and return after two seconds. We will not see an explosion because we’re removing the current player before it gets a chance to explode. That is intentional. Trust me.

As expected, we see no explosion. Improve our test. What do we need? We need a new object: a ship explosion. That seems a bit much somehow. It’s really very simple though, isn’t it?

I scrabble around for something “easier”. We could defer the removal until after this player is done exploding. We could create a new player instance and set a flag that makes it just explode.

Or we could have a PlayerExplosion.

It’s the right thing to do. And we are already almost 400 lines into this article, so maybe it’s too much for this time.

Let’s do something simpler and then sum up. Here are the stories as I now see them:

  1. When exploding, the player should not be able to move or fire.
  2. [After exploding], the player should add a TimeCapsule containing a new ship, and
  3. should remove itself.
  4. The TimeCapsule should wait for some time—try one second— and then add a Player.
  5. The fleet should freeze when there is no player.
  6. The player should start at far left.

I think #1 is a given if we keep the current idea of a separate PlayerExplosion. Far left looks like the next thing to do, so let’s do that and call it a wrap.

How far is far left?

    def __init__(self):
        maker = BitmapMaker.instance()
        self.players = maker.players  # one turret, two explosions
        self.player = self.players[0]
        self._mask = pygame.mask.from_surface(self.player)
        self._rect = self.player.get_rect()
        self.rect.center = Vector2(u.SCREEN_SIZE/2, u.SCREEN_SIZE - 5*32 - 16)
        self.step = 4
        half_width = self.rect.width / 2
        self.left = 64 + half_width
        self.right = 960 - half_width
        self.free_to_fire = True
        self.fire_request_allowed = True
        self.explode_time = 0

Ha. Interesting. The InvaderPlayer knows its limitations. (A man has to know his limitations.) So we can set him to his left:

    def __init__(self):
        maker = BitmapMaker.instance()
        self.players = maker.players  # one turret, two explosions
        self.player = self.players[0]
        self._mask = pygame.mask.from_surface(self.player)
        self._rect = self.player.get_rect()
        self.step = 4
        half_width = self.rect.width / 2
        self.left = 64 + half_width
        self.right = 960 - half_width
        self.rect.center = Vector2(self.left, u.SCREEN_SIZE - 5*32 - 16)
        self.free_to_fire = True
        self.fire_request_allowed = True
        self.explode_time = 0

That should do the trick. Should we test that? Probably but first I want to see it in the game.

Note:
If you do not see a video above, please consider clearing your browser cache and trying again. If that fails to show the video please toot me on mastodon. Toot. I get it but really …

Summary

I’ve forgotten some commits. Commit: PLayer now vanishes on hit, new player arrives after two seconds.

We have decent tests for the TimeCapsule. Our test for the PLayer turned out to be really nice: we extracted a new method hit_by_shot, which does the necessary appends and removes to Fleets … and we passed in a FakeFleets instance that the test uses to look for things. That’s a bit more direct than using the real Fleets and a FleetsInspector (FI) to look for things not being there. I am pleased with how nicely that avoided the longer story-test kinds of tests.

I with I had thought of it sooner. I also wish I had not bought that used Lamborghini, but past is past. I’m glad that I have this better way of testing now, but I still wish I had the $30,000 back. (It was a long time ago and not a very good Lamborghini.)

Our stories are now something like this:

  1. When exploding, the player should not be able to move or fire.
  2. [After exploding], the player should add a TimeCapsule containing a new ship, and
  3. should remove itself.
  4. The TimeCapsule should wait for some time—try one second— and then add a Player.
  5. The fleet should freeze when there is no player.
  6. The player should start at far left.
  7. There should be an explosion after player is hit and before a new player arrives.
  8. There should be some finite number of players in a game, probably 3.
  9. After the players are all consumed, return to the top level game over screen?
  10. Possibly we need our own game-over screen so as to show the score? Needs consideration.

I think this has gone rather nicely so far. We do not have a test for player starting on the left. Trivial to do, I guess:

class TestPlayer:
    def test_start_left(self):
        player = InvaderPlayer()
        assert player.rect.centerx == player.left

Commit: test player starts at left.

OK, so. what have we got, what we learned?

I am pleased that I got into the testing rhythm. I honestly feel more confident even with those trivial tests and they have the added advantage of letting me do most of my coding without running the game, just to see if things work. By the time I run the game, I’m pretty sure that most things do work.

The tests are not perfect: there was an issue that wasn’t checked in testing. What was it? I honestly do not quite remember. I think Fleets sent some cycle message that is not required in Flyer and is required in the AsteroidsFlyer and InvaderFlyer interfaces. I wish I had mentioned it: it would be useful to add some more explicit testing. The mistake, of course, was trying to “save time” by inheriting from Flyer instead of the right interface.

I do think there is a possibility that we could devise a better interface hierarchy that better balances ease of new object creation and certainty that objects implement what they need to. My colleagues tell me that an object that describes interactions pair-wise might be better. It might. Anyway something could be improved. We may or may not invest in that at some future time.

Today, we have a nice new object, TimeCapsule., that can insert any object into the mix at a defined future time. On the down-side, I think it’s only usable as an InvadersFlyer. We’d need some middle-ground Flyers interface for objects that can work on more than one game. Might be useful someday, but I don’t think we’ll go back and retrofit Asteroids with our new learning … unless we decide to enhance it in some important way.

Important:
I should mention: that’s my real starting advice for code refactoring and improvement. I think in a real project we should do our improvements where we do our work, and should minimize effort improving code that would otherwise be left alone. This article, Refactoring - Not on the backlog! sums up my view pretty well.

We now have the game removing the player when hit, restoring it two seconds later. Our next step, next time, will probably be to create a little player explosion object, toss it in, and watch the fun.

I’ll hope to see you then!



  1. I do not, however, give advice. I used to sell advice, but now I just keep it to myself and let you watch what happens to me when I do things one way or another. 

  2. Do you know how to tell the difference between a crocodile and an alligator? One you’ll see in a while, and the other one you’ll see later. (Mitch Hedberg, RIP)