Let’s do safe entry. Should I put this thing up on GitHub? No one has asked for that.

The basic rules for safe entry of a respawning ship are that there should be no missiles flying, and no asteroids within some distance of the center, where the ship will emerge. What is that distance? Um … in one previous version, it’s universe size over ten, but let’s make it a standard 100.

We’ll test a new function in main, safe_to_emerge.

    def test_safe_to_emerge_hates_missiles(self):
        missiles = []
        asteroids = []
        assert safe_to_emerge(missiles, asteroids)
        missiles.append(Missile(Vector2(0, 0), Vector2(0, 0)))
        assert not safe_to_emerge(missiles, asteroids)

And:

def safe_to_emerge(missiles, asteroids):
    if missiles: return False
    return True

The test passes. Shall we commit? Not yet, but let’s plug the function in and then commit.

def check_ship_spawn(ship, ships, delta_time):
    if ships: return
    global ship_timer
    ship_timer -= delta_time
    if ship_timer <= 0 and safe_to_emerge(missiles, asteroids):
        ship.reset()
        ships.append(ship)

I kind of suspect we should have a test for this function’s proper use of safe_to_emerge, but that means we need to pass in the other collections, or else we can’t test it other than by hammering the globals. Nasty. We’ll defer that.

I want to see this working in the game, if possible. Can’t see it very well with missiles only lasting 3 seconds, they’re guaranteed to be gone. I don’t care, I’m confident that it works because my test works. If we were to fiddle with the numbers it would work properly, I’m sure.

Now the other test, for nearby asteroids:

    def test_safe_to_emerge_hates_close_asteroids(self):
        asteroids = []
        missiles = []
        assert safe_to_emerge(missiles, asteroids)
        asteroids.append(Asteroid(2, u.CENTER))
        assert not safe_to_emerge(missiles, asteroids)

Test is red. Improve code:

def safe_to_emerge(missiles, asteroids):
    if missiles: return False
    for asteroid in asteroids:
        if asteroid.position.distance_to(u.CENTER) < 100:
            return False
    return True

We’re good except for some magic numbers. I’ll add a few:

ASTEROID_SPEED = pygame.Vector2(100,0)
SCREEN_SIZE = 768
CENTER = pygame.Vector2(SCREEN_SIZE/2, SCREEN_SIZE/2)
SAFE_EMERGENCE_DISTANCE = 100 # new
SHIP_ACCELERATION = pygame.Vector2(120, 0)
SHIP_EMERGENCE_TIME = 3 # new
SHIP_ROTATION_STEP = 120
SPEED_OF_LIGHT = 500
MISSILE_SPEED = SPEED_OF_LIGHT/3
MISSILE_LIFETIME = 3 # new
MISSILE_LIMIT = 4 # new

Now to plug them in:

def safe_to_emerge(missiles, asteroids):
    if missiles: return False
    for asteroid in asteroids:
        if asteroid.position.distance_to(u.CENTER) < u.SAFE_EMERGENCE_DISTANCE:
            return False
    return True

def check_asteroids_vs_ship():
    for ship in ships.copy():  # there's only one, do it first
        for asteroid in asteroids.copy():
            asteroid.collide_with_attacker(ship, ships, asteroids)
            if not ships:
                set_ship_timer(u.SHIP_EMERGENCE_TIME)
                return

    def update(self, missiles, delta_time):
        self.time += delta_time
        if self.time > u.MISSILE_LIFETIME:
            missiles.remove(self)

So that’s nice. I’m not using MISSILE_LIMIT yet. We are green. Commit: ship emerges only if it’s safe to do so.

Now what about the MISSILE_LIMIT? I intend that to limit the number of simultaneous missiles to four, which seems to be the limit in the original game. Shall we write a test? OK, but let’s see how missile firing works first.

    def fire_if_possible(self, missiles):
        if self.can_fire:
            missiles.append(Missile(self.missile_start(), self.missile_velocity()))
            self.can_fire = False

The can_fire flag is toggled by the firing key, “k” rising. But we can test like this:

    def test_firing_limit(self):
        ship = Ship(u.CENTER)
        missiles = []
        ship.fire_if_possible(missiles)
        assert len(missiles) == 1
        ship.can_fire = True
        ship.fire_if_possible(missiles)
        assert len(missiles) == 2
        ship.can_fire = True
        ship.fire_if_possible(missiles)
        assert len(missiles) == 3
        ship.can_fire = True
        ship.fire_if_possible(missiles)
        assert len(missiles) == 4
        ship.can_fire = True
        ship.fire_if_possible(missiles)
        assert len(missiles) == 4

Hmm, that should really check the limit, not assume 4. We’ll fix that in a moment. The test is red and I think we can make it green:

    def fire_if_possible(self, missiles):
        if self.can_fire and len(missiles) < u.MISSILE_LIMIT:
            missiles.append(Missile(self.missile_start(), self.missile_velocity()))
            self.can_fire = False

Let’s improve the test:

    def test_firing_limit(self):
        ship = Ship(u.CENTER)
        count = 0
        missiles = []
        while len(missiles) < u.MISSILE_LIMIT:
            ship.can_fire = True
            ship.fire_if_possible(missiles)
            count += 1
            assert len(missiles) == count
        assert len(missiles) == u.MISSILE_LIMIT
        ship.can_fire = True
        ship.fire_if_possible(missiles)
        assert len(missiles) == u.MISSILE_LIMIT

We fill the missiles by actual firing up to the limit, then assert that we’ve got that many many missiles, then try to fire one more and fail. I like it.

Green. I like auto-run too. Commit: Ship can fire only u.MISSILE_LIMIT missiles at a time.

Let’s sum up.

Summary

We’ve done two quick features, safe emergence and applying a limit to the number of simultaneous missiles. They went in easily and without difficulty except that the tests were running in an infinite loop at one point, when auto-run started them before I had the test loop right. I just stopped them and they immediately ran green.

I’m liking these new tests, and I’m liking how easily the features have gone in. Some future ones may be a bit more tricky. We’ll need to ensure that you don’t get scoring credit for crashing into an asteroid, when we do scoring (unless we decide to allow that sort of Pyrrhic victory, and even then there will be Saucer bullets to deal with). That may be a bit tricky since we’ll have to know what’s colliding with the asteroid. I don’t foresee a big issue.

Most everything looks easy to me at the moment. This might be because I’ve done this N times, but I think it’s also easy because the design, while simple, is, well, simple and reasonable, with capabilities fairly nicely isolated.

I’d like to have a direct test of this function:

def check_ship_spawn(ship, ships, delta_time):
    if ships: return
    global ship_timer
    ship_timer -= delta_time
    if ship_timer <= 0 and safe_to_emerge(missiles, asteroids):
        ship.reset()
        ships.append(ship)

To make that reasonable, we should pass the missiles and asteroids collections into that function. I don’t quite want to do that just now, I’ve done enough for this session and I think the signature change will be a bit tricky.

So, not bad for a quick set of improvements. We’re getting the hang of this!

See you next time!