Python Asteroids on GitHub

In for a penny and all that, I do the Ship’s spawning Timer, somewhat the same but slightly different. I mention a Fundamental Learning.

Right.

    # noinspection PyAttributeOutsideInit
    def init_asteroids_game_values(self):
        self.asteroids_in_this_wave: int
        self.init_saucer_timer()
        self.ship_timer = 0
        self.ships_remaining = 0
        self.init_wave_timer()

Pretty sure that ship_timer there needs to be a timer. We’ll check the other references to it:

class Game:
    def check_ship_spawn(self, ship, ships, delta_time):
        if ships: return
        if self.ships_remaining <= 0:
            self.game_over = True
            return
        self.ship_timer -= delta_time
        if self.ship_timer <= 0 and self.safe_to_emerge(self.missiles, self.asteroids):
            ship.reset()
            ships.append(ship)
            self.ships_remaining -= 1

    def set_ship_timer(self, seconds):
        if self.ship_timer <= 0:
            self.ship_timer = seconds

What’s that last one?

class Game:
    def process_collisions(self):
        collider = Collider(asteroids=self.asteroids, missiles=self.missiles, saucers=self.saucers, saucer_missiles=self.saucer_missiles,
                            ships=self.ships)
        self.score += collider.check_collisions()
        if not self.ships:
            self.set_ship_timer(u.SHIP_EMERGENCE_TIME)

    def insert_quarter(self, number_of_ships):
        self.asteroids = []
        self.missiles = []
        self.ships = []
        self.asteroids_in_this_wave = 2
        self.game_over = False
        self.init_saucer_timer()
        self.score = 0
        self.ships_remaining = number_of_ships
        self.set_ship_timer(u.SHIP_EMERGENCE_TIME)
        self.init_wave_timer()
        self.delta_time = 0


class TestCollisions:
    def test_respawn_ship(self):
        game = Game(testing=True)
        game.ships_remaining = 3
        ship = Ship(Vector2(0, 0))
        ship.velocity = Vector2(31, 32)
        ship.angle = 90
        ships = []
        game.set_ship_timer(3)
        assert game.ship_timer == 3
        game.check_ship_spawn(ship, ships, 0.1)
        assert not ships
        game.check_ship_spawn(ship, ships, u.SHIP_EMERGENCE_TIME)
        assert ships
        assert ship.position == u.CENTER
        assert ship.velocity == Vector2(0, 0)
        assert ship.angle == 0

You’d think that if we were going to have the set_ship_timer thing, we’d use it all the time. However, with that if in it, I can’t call it from the first usage init_asteroids_game_values, because the if will find the timer unset. Let’s just plug in the timer., in the set method, and then call it. Tests will break for a while.

    # noinspection PyAttributeOutsideInit
    def init_asteroids_game_values(self):
        self.asteroids_in_this_wave: int
        self.init_saucer_timer()
        self.set_ship_timer(u.SHIP_EMERGENCE_TIME)
        self.ships_remaining = 0
        self.init_wave_timer()

    def set_ship_timer(self, seconds):
        self.ship_timer = Timer(seconds, self.spawn_ship_when_ready)

We’re good in insert_quarter. Any other references to ship_timer? Just here:

    def check_ship_spawn(self, ship, ships, delta_time):
        if ships: return
        if self.ships_remaining <= 0:
            self.game_over = True
            return
        self.ship_timer -= delta_time
        if self.ship_timer <= 0 and self.safe_to_emerge(self.missiles, self.asteroids):
            ship.reset()
            ships.append(ship)
            self.ships_remaining -= 1

We need to extract the working bit:

    def check_ship_spawn(self, ship, ships, delta_time):
        if ships: return
        if self.ships_remaining <= 0:
            self.game_over = True
            return
        self.ship_timer -= delta_time
        self.spawn_ship_when_ready(ship, ships)

    def spawn_ship_when_ready(self, ship, ships):
        if self.ship_timer <= 0 and self.safe_to_emerge(self.missiles, self.asteroids):
            ship.reset()
            ships.append(ship)
            self.ships_remaining -= 1

Then:

    def check_ship_spawn(self, ship, ships, delta_time):
        if ships: return
        if self.ships_remaining <= 0:
            self.game_over = True
            return
        self.ship_timer.tick(delta_time, ship, ships)

    def spawn_ship_when_ready(self, ship, ships):
        if not self.safe_to_emerge(self.missiles, self.asteroids):
            return False
        ship.reset()
        ships.append(ship)
        self.ships_remaining -= 1
        return True

One test failing, but the game should be right. It isn’t: the ship never shows up. Better look at the test. I just needed to remove its reference to ship_timer as an integer.

class TestCollisions:
    def test_respawn_ship(self):
        game = Game(testing=True)
        game.ships_remaining = 3
        ship = Ship(Vector2(0, 0))
        ship.velocity = Vector2(31, 32)
        ship.angle = 90
        ships = []
        game.set_ship_timer(u.SHIP_EMERGENCE_TIME)
        game.check_ship_spawn(ship, ships, 0.1)
        assert not ships
        game.check_ship_spawn(ship, ships, u.SHIP_EMERGENCE_TIME)
        assert ships
        assert ship.position == u.CENTER
        assert ship.velocity == Vector2(0, 0)
        assert ship.angle == 0

That runs green. But why oh why is the ship not appearing when I put in my quarter?

It’s this:

    def process_collisions(self):
        collider = Collider(asteroids=self.asteroids, missiles=self.missiles, saucers=self.saucers, saucer_missiles=self.saucer_missiles,
                            ships=self.ships)
        self.score += collider.check_collisions()
        if not self.ships:
            self.set_ship_timer(u.SHIP_EMERGENCE_TIME)

What is this exactly, belt and suspenders? What happens if we just remove that last bit?

Right, everything works fine. I have no idea why I put that check there. Commit: Game uses Timer for ship spawning.

Saucer Missile Timer

Are there more timers up in this thing? I don’t think so but I’ll search and explore. Searching for “timer”, I find this:

    def missile_timer_expired(self, delta_time):
        self.missile_timer -= delta_time
        expired = self.missile_timer <= 0
        return expired

That’s in saucer. Who calls it? Command+B.

    def firing_is_possible(self, delta_time, saucer_missiles):
        return self.missile_timer_expired(delta_time) and self.a_missile_is_available(saucer_missiles)

Who calls that?

    def fire_if_possible(self, delta_time, saucer_missiles, ships):
        if self.firing_is_possible(delta_time, saucer_missiles):
            self.fire_a_missile(saucer_missiles, ships)

OK, who calls that?

        if keys[pygame.K_k]:
            ship.fire_if_possible(self.missiles)

OK, we have here a legitimate timing going on that’s not using the Timer. Can we ball-peen this until it does? I should think so.

Let’s look at the whole flow, it does go on and on …

    def fire_if_possible(self, delta_time, saucer_missiles, ships):
        if self.firing_is_possible(delta_time, saucer_missiles):
            self.fire_a_missile(saucer_missiles, ships)

    def firing_is_possible(self, delta_time, saucer_missiles):
        return self.missile_timer_expired(delta_time) and self.a_missile_is_available(saucer_missiles)

    def missile_timer_expired(self, delta_time):
        self.missile_timer -= delta_time
        expired = self.missile_timer <= 0
        return expired

    @staticmethod
    def a_missile_is_available(saucer_missiles):
        return len(saucer_missiles) < u.SAUCER_MISSILE_LIMIT

    def fire_a_missile(self, saucer_missiles, ships):
        saucer_missiles.append(self.create_missile(ships))
        self.missile_timer = u.SAUCER_MISSILE_DELAY

There must be another setter for missile_timer. Yes, in the Saucer’s __init__.

Let’s just do our usual thing, creating set_missile_timer and using it.

class Saucer:
    def __init__(self, position=None, size=2):
        self.position = position if position is not None else u.CENTER
        self.size = size
        self.velocity = u.SAUCER_VELOCITY
        self.directions = (self.velocity.rotate(45), self.velocity, self.velocity, self.velocity.rotate(-45))
        self.direction = -1
        self.radius = 20
        raw_dimensions = Vector2(10, 6)
        saucer_scale = 4 * self.size
        self.offset = raw_dimensions * saucer_scale / 2
        saucer_size = raw_dimensions * saucer_scale
        self.saucer_surface = SurfaceMaker.saucer_surface(saucer_size)
        self.set_firing_timer()
        self.set_zig_timer()

    def set_firing_timer(self):
        self.missile_timer = Timer(u.SAUCER_MISSILE_DELAY, self.fire_if_possible)

    def ready(self):
        self.direction = -self.direction
        self.velocity = self.direction * u.SAUCER_VELOCITY
        x = 0 if self.direction > 0 else u.SCREEN_SIZE
        self.position = Vector2(x, random.randrange(0, u.SCREEN_SIZE))
        self.set_firing_timer()
        self.set_zig_timer()

That brings me here:

    def fire_if_possible(self, delta_time, saucer_missiles, ships):
        if self.firing_is_possible(delta_time, saucer_missiles):
            self.fire_a_missile(saucer_missiles, ships)

    def firing_is_possible(self, delta_time, saucer_missiles):
        return self.missile_timer_expired(delta_time) and self.a_missile_is_available(saucer_missiles)

    def missile_timer_expired(self, delta_time):
        self.missile_timer -= delta_time
        expired = self.missile_timer <= 0
        return expired

I recall—actually I look to see that—the Timer is going to call fire_if_possible, so we know that the timer will be expired, so we don’t have to check that … we don’t want to call fire_if_possible, that’s where we want to tick. We want to call fire_if_missile_available which will return T/F.

    def set_firing_timer(self):
        self.missile_timer = Timer(u.SAUCER_MISSILE_DELAY, self.fire_if_missile_available)

    def fire_if_missile_available(self, saucer_missiles, ships):
        if self.a_missile_is_available(saucer_missiles):
            self.fire_a_missile(saucer_missiles, ships)
            return True
        else:
            return False

Tests are red but this should—and does—work in the game.

I can remove some excess code:


    def firing_is_possible(self, delta_time, saucer_missiles):
        return self.missile_timer_expired(delta_time) and self.a_missile_is_available(saucer_missiles)

    def missile_timer_expired(self, delta_time):
        self.missile_timer -= delta_time
        expired = self.missile_timer <= 0
        return expired

Now about those tests:

class TestSaucer:
    def test_ready(self):
        saucer = Saucer()
        saucer.ready()
        assert saucer.position.x == 0
        assert saucer.velocity == u.SAUCER_VELOCITY
        assert saucer.missile_timer == u.SAUCER_MISSILE_DELAY
        saucer.missile_timer = 0
        saucer.ready()
        assert saucer.position.x == u.SCREEN_SIZE
        assert saucer.velocity == -u.SAUCER_VELOCITY
        assert saucer.zig_timer.delay == u.SAUCER_ZIG_TIME
        assert saucer.zig_timer.elapsed == 0
        assert saucer.missile_timer == u.SAUCER_MISSILE_DELAY

Those checks for the SAUCER_MISSILE_DELAY won’t do. Shall we reach down in and check the timer? It shouldn’t be necessary, but let’s do:

This passes:

    def test_ready(self):
        saucer = Saucer()
        saucer.ready()
        assert saucer.position.x == 0
        assert saucer.velocity == u.SAUCER_VELOCITY
        assert saucer.missile_timer.elapsed == 0
        saucer.missile_timer.elapsed = 0.5
        saucer.ready()
        assert saucer.position.x == u.SCREEN_SIZE
        assert saucer.velocity == -u.SAUCER_VELOCITY
        assert saucer.zig_timer.delay == u.SAUCER_ZIG_TIME
        assert saucer.zig_timer.elapsed == 0
        assert saucer.missile_timer.elapsed == 0

Pretty invasive but does check to be sure that ready resets the missile_timer.

And the other test:

    def test_can_only_fire_two(self):
        saucer = Saucer()
        saucer_missiles = []
        assert saucer.missile_timer == u.SAUCER_MISSILE_DELAY
        saucer.fire_if_possible(delta_time=0.1, saucer_missiles=saucer_missiles, ships=[])
        assert not saucer_missiles
        saucer.fire_if_possible(u.SAUCER_MISSILE_DELAY, saucer_missiles=saucer_missiles, ships=[])
        assert len(saucer_missiles) == 1
        assert saucer.missile_timer == u.SAUCER_MISSILE_DELAY
        saucer.fire_if_possible(u.SAUCER_MISSILE_DELAY, saucer_missiles=saucer_missiles, ships=[])
        assert len(saucer_missiles) == 2
        saucer.fire_if_possible(u.SAUCER_MISSILE_DELAY, saucer_missiles=saucer_missiles, ships=[])
        assert len(saucer_missiles) == 2

I think I’ll just remove the checks for the timer.

    def test_can_only_fire_two(self):
        saucer = Saucer()
        saucer_missiles = []
        saucer.fire_if_possible(delta_time=0.1, saucer_missiles=saucer_missiles, ships=[])
        assert not saucer_missiles
        saucer.fire_if_possible(u.SAUCER_MISSILE_DELAY, saucer_missiles=saucer_missiles, ships=[])
        assert len(saucer_missiles) == 1
        saucer.fire_if_possible(u.SAUCER_MISSILE_DELAY, saucer_missiles=saucer_missiles, ships=[])
        assert len(saucer_missiles) == 2
        saucer.fire_if_possible(u.SAUCER_MISSILE_DELAY, saucer_missiles=saucer_missiles, ships=[])
        assert len(saucer_missiles) == 2

Perfectly good firing test. I think it could be simpler now but this way involves the Timer and that’s good. I could just call the Timer’s action, but I think this is a more solid test.

We are green. Commit: saucer missiles fired using Timer.

Reflection

Now I think I’ll look at references to delta_time to see if anyone is counting with it. There are lots of references, but only one that says += delta_time. Everyone else calls a tick with it, or multiplies it times a velocity or the like.

There are, I’m rather sure, no wayward timers who are not using Timer. I think that’s a good thing. Let’s sum up.

Summary

Making these changes has been pretty quick and generally easy, with one common exception that I’ll mention below. But it wasn’t done in a single change, extract this refactor that kind of way. It was basically programming, and I’d have preferred it to be more mechanical.

I don’t see how to have done that. So I programmed.

Common Exception

I’m troubled by the repeated trope of setting the timer in __init__ and then again in a ready operation or insert_quarter. I stumbled over that change at least twice and I think more times.

It would be better, I think, if the Timer had a reset operation to set elapsed back to zero, and if the ready and insert_quarter were to use that. But even the need to initialize it a second time at all is an issue but since the game can restart with the timers in any random state, resetting is the best idea I have so far. I think I’d have stumbled anyway.

Part of the problem is that because Python wants me to init everything at the beginning, and because I want to start at GAME OVER, I have to reset things when you put in your quarter. Maybe there’s a way around that. I’ll look for it … but not now.

We won’t even put in reset now, just because we have another 400 line article here and no one wants even one of those in a day, much less two.

Fundamental Learning

What we are seeing here in this continuous stream of articles is a single key point: we can always, with a little bit of work, make the code better.

I’ll push this one and hope you’ll stop by next time! See you then!