Python Asteroids+Invaders on GitHub

We have a start at player shots. Let’s get one on the screen .. and then off the screen.

We have the first player shot test running:

    def test_can_fire_initially(self):
        fleets = Fleets()
        fi = FI(fleets)
        player = InvaderPlayer()
        fleets.append(player)
        player.attempt_firing(fleets)
        assert fi.player_shots

This just checks that there’s a shot in the fleets object. The test is supported by this minimal code:

class FleetsInspector:
    @property
    def player_shots(self):
        return self.select_class(PlayerShot)

class InvaderFlyer(InvadersFlyer):
    def attempt_firing(self, fleets):
        fleets.append(PlayerShot())

Just enough, not a bit more. Let’s add a test.The player can have only one shot on screen at a time. We can express that like this:

    def test_cannot_fire_with_one_on_screen(self):
        fleets = Fleets()
        fi = FI(fleets)
        player = InvaderPlayer()
        fleets.append(player)
        player.attempt_firing(fleets)
        assert len(fi.player_shots) == 1
        player.attempt_firing(fleets)
        assert len(fi.player_shots) == 1

This fails, but it’s not really good as a test. Here’s why.

The player needs to know whether there is a shot already in the mix. Now, it is true that since it has access to the fleets object, it could check directly. But that is not how we do things in this design. We detect things during interactions. So the full sequence of events needs to be like this:

    def test_cannot_fire_with_one_on_screen(self):
        fleets = Fleets()
        fi = FI(fleets)
        player = InvaderPlayer()
        fleets.append(player)
        player.begin_interactions(fleets)
        player.attempt_firing(fleets)
        assert len(fi.player_shots) == 1
        shot = fi.player_shots[0]
        player.begin_interactions(fleets)
        player.interact_with_playershot(shot, fleets)
        player.attempt_firing(fleets)
        assert len(fi.player_shots) == 1

In the first attempt, we do not see an interact_with_playershot and in the second, we do. That is the “signal” that the player has to use. Like this:

class InvaderPlayer(InvadersFlyer):

    def __init__(self):
        maker = BitmapMaker.instance()
        ...
        self.free_to_fire = True

    def begin_interactions(self, fleets):
        self.free_to_fire = True

    def interact_with_playershot(self, bumper, fleets):
        self.free_to_fire = False

    def attempt_firing(self, fleets):
        if self.free_to_fire:
            fleets.append(PlayerShot())

Test is green. Commit: player only fires if no shots on screen.

There is another issue, however. We have no way to fire a missile as the human player. The controls do not yet support that. Here’s the relevant code:

class InvaderPlayer(InvadersFlyer):
    def update(self, _delta_time, _fleets):
        if not pygame.get_init():
            return
        keys = pygame.key.get_pressed()
        if keys[pygame.K_f]:
            self.move(self.step)
        elif keys[pygame.K_d]:
            self.move(-self.step)

We want to require the firing key, which will be K, to be tapped once for each firing. That is, we won’t just fire because it is down, we’ll require it to be up before we fire again.

We need another flag.

    def update(self, _delta_time, fleets):
        if not pygame.get_init():
            return
        keys = pygame.key.get_pressed()
        if keys[pygame.K_f]:
            self.move(self.step)
        elif keys[pygame.K_d]:
            self.move(-self.step)
        self.check_trigger(keys, fleets)

    def check_trigger(self, keys, fleets):
        if keys[pygame.K_k]:
            if self.trigger_released:
                self.trigger_released = False
                self.attempt_firing(fleets)
        else:
            self.trigger_released = True

Having written that, I think I can test it. Before I wrote it, I didn’t see how. Now I can at least try. I’m still having trouble writing the test. Let me first express the logic in code. Maybe that will suggest a test:

    def update(self, _delta_time, fleets):
        if not pygame.get_init():
            return
        keys = pygame.key.get_pressed()
        ...
        if not keys[pygame.K_k]:
            self.trigger_released = True
        else:
            self.attempt_firing(fleets)

    def attempt_firing(self, fleets):
        if self.trigger_released and self.free_to_fire:
            fleets.append(PlayerShot())
        self.trigger_released = False

Ignore that code for a moment. My thoughts are clearing.

Here’s a cut at a test for trigger logic:

    def test_trigger_logic(self):
        player = InvaderPlayer()
        assert player.fire_request_allowed
        player.trigger_pulled()
        assert not player.fire_request_allowed
        player.trigger_released()
        assert player.fire_request_allowed

Player starts with firing allowed, when the trigger is pulled, firing is no longer allowed, when it is released, it is allowed again.

Let’s make that work.

    def trigger_pulled(self):
        self.fire_request_allowed = False

    def trigger_released(self):
        self.fire_request_allowed = True

Trivial, of course. Now let’s wire the keys to it, while it’s on my mind:

    def update(self, _delta_time, fleets):
        if not pygame.get_init():
            return
        keys = pygame.key.get_pressed()
        if keys[pygame.K_f]:
            self.move(self.step)
        elif keys[pygame.K_d]:
            self.move(-self.step)
        if not keys[pygame.K_k]:
            self.trigger_pulled()
        else:
            self.trigger_released()

Now we need one more thing. If the trigger is pulled when fire request is allowed, we should attempt to fire. (We might still not succeed, if there is a shot in flight).

Let me refactor a bit before I show you this.

    def update(self, _delta_time, fleets):
        if not pygame.get_init():
            return
        keys = pygame.key.get_pressed()
        self.check_motion(keys)
        self.check_firing(fleets, keys)

    def check_firing(self, fleets, keys):
        if not keys[pygame.K_k]:
            self.trigger_pulled(fleets)

    def trigger_pulled(self, fleets):
        if self.fire_request_allowed:
            self.attempt_firing(fleets)
        self.fire_request_allowed = False

    def trigger_released(self):
        self.fire_request_allowed = True

    def attempt_firing(self, fleets):
        if self.free_to_fire:
            fleets.append(PlayerShot())

Notice that I need fleets in the trigger_pulled. Had to put that in the test also.

I think I like these tests now.

    def test_can_fire_initially(self):
        fleets = Fleets()
        fi = FI(fleets)
        player = InvaderPlayer()
        fleets.append(player)
        player.attempt_firing(fleets)
        assert fi.player_shots

    def test_cannot_fire_with_one_on_screen(self):
        fleets = Fleets()
        fi = FI(fleets)
        player = InvaderPlayer()
        fleets.append(player)
        player.begin_interactions(fleets)
        player.attempt_firing(fleets)
        assert len(fi.player_shots) == 1
        shot = fi.player_shots[0]
        player.begin_interactions(fleets)
        player.interact_with_playershot(shot, fleets)
        player.attempt_firing(fleets)
        assert len(fi.player_shots) == 1

    def test_trigger_logic(self):
        fleets = Fleets()
        player = InvaderPlayer()
        assert player.fire_request_allowed
        player.trigger_pulled(fleets)
        assert not player.fire_request_allowed
        player.trigger_released()
        assert player.fire_request_allowed

There is no test that checks that if fire request is not allowed, no missile is emitted. The other tests all take advantage of the fact that it’s allowed unless we turn it off.

I think we can write a suitable test:

    def test_firing_with_trigger(self):
        fleets = Fleets()
        fi = FI(fleets)
        player = InvaderPlayer()
        player.trigger_pulled(fleets)
        assert fi.player_shots
        fleets.clear()
        player.trigger_pulled(fleets)
        assert not fi.player_shots
        player.trigger_released()
        player.trigger_pulled(fleets)
        assert fi.player_shots

I am pleased. Reasonably clear tests and pretty much all the conditional logic is checked. I do wonder about a better way, but this is decent. Commit: test and implement trigger logic in player.

Now I’d really like to get a shot on the screen and fly it.

Let’s begin by rezzing it and making it visible. We’ll need a starting location when we create it, and I just noticed that I haven’t put the shot into the bitmaps yet. We’ll deal with that in a moment.First I want a location and a draw of any kind.

class PlayerShot(InvadersFlyer):
    def __init__(self, position=u.CENTER):
        self.position = MovableLocation(position, Vector2(0, 0))
        maker = BitmapMaker.instance()

    def draw(self, screen):
        center = self.position.position
        pygame.draw.circle(screen, "red", center, 20)

I am somewhat surprised to find a red circle at the center of the screen as soon as I start the game:

surprise red circle

Who fired the first shot?1 This code is backward:

    def check_firing(self, fleets, keys):
        if not keys[pygame.K_k]:
            self.trigger_pulled(fleets)
        else:
            self.trigger_released()

Should be:

    def check_firing(self, fleets, keys):
        if keys[pygame.K_k]:
            self.trigger_pulled(fleets)
        else:
            self.trigger_released()

My excellent tests do not use check_firing as well as they might, because I didn’t want to set up the keys. Might should do that.

Now when I type K, the dot appears. Let’s put it where it belongs.

    def attempt_firing(self, fleets):
        if self.free_to_fire:
            fleets.append(PlayerShot(self.rect.center))

That should put the dot right on top of the player. It does:

player moved, dot on top

Now the shot should rez a bit above its input position and it should have a velocity upward. A little fiddling, and I get the following code. I decided not to bother with the MovableLocation stuff. We’ll think about that later, perhaps.

class PlayerShot(InvadersFlyer):
    def __init__(self, position=u.CENTER):
        offset = Vector2(0, -8*4)
        self.position = position + offset
        self.velocity = Vector2(0, -4*4)
        maker = BitmapMaker.instance()

    def draw(self, screen):
        center = self.position
        rect = pygame.Rect(0, 0, 4, 32)
        rect.center = center
        pygame.draw.rect(screen, "white", rect)

That looks just about right:

firing missiles

I’ve been at this nearly two hours. Let’s commit and sum up. Commit: player appears to fire shot. not using real bitmap, no collisions. just visual effect.

Summary

Decent tests

I created some pretty decent tests for the keyboard firing logic, but they’re still not as robust as they might be, because while I do check the interlocking logic, I do not drive the tests from actual key inputs. And that bit me because I had the key inputs upside down.

I’ve made a note to test that. It needs a bit more thought than I have available right now.

Logic seemed tricky

I didn’t see at first how to set up the firing logic to be readily testable. A mockist-style tester might have been better off, as they’ll usually test to see that the right calls are made. I am more inclined to test state, so when I finally got the states separated out as a couple of flags, testing got easier. I think the tests are better than my usual ones for dealing with input events. That’s not to say that they’re great, but better is better.

Questionable decision

I elected not to use the MovableLocation object that is used in Asteroids. We should look into that, but if we manage our own positions, that will certainly work. I certainly have mixed feelings about it, but simpler seemed, well, simpler.

Looking good

The current visual effect of firing is a step in the right direction. We create a real PlayerShot instance and fly it off the screen. There are magic numbers that need to be less magical and more accurate. We need to bring in the shot bitmap and use it. We need the shot explosion at the top of the screen.

A decent outcome

All that will come. We now have a nicely improved demo and all the real caveats are “not done yet”, not “done incorrectly”. The code seems to me to be incomplete but structured much as we would want it to be.

I felt a little ragged today, but I think we have a reasonably solid situation. Improvements are possible and probably needed, but I think everything is in just about the right place.

See you next time!



  1. Han clearly fired first. Retconning it isn’t fair.