Let’s see if we can make this thing fire a missile.

First, I think I’ll make a Missile object, much like an Asteroid. I’m not going to TDD it, it’s the afternoon and I just don’t feel like it.

# Missile

import pygame
from mover import Mover


class Missile:
    def __init__(self, position, velocity):
        self.radius = 2
        self.mover = Mover(position, velocity)

    def draw(self, screen):
        pygame.draw.circle(screen, "white", self.mover.position, 2)

I wonder how we’ll fire them. Let’s review the controls:

    keys = pygame.key.get_pressed()
    if keys[pygame.K_f]:
        ship.turn_left(dt)
    if keys[pygame.K_d]:
        ship.turn_right(dt)
    if keys[pygame.K_j]:
        ship.power_on(dt)
    else:
        ship.power_off()

I think we’ll just pass in a missiles collection. Let’s create one:

ship = Ship(pygame.Vector2(u.SCREEN_SIZE / 2, u.SCREEN_SIZE / 2))
asteroids = [Asteroid(2) for i in range(0, 4)]
missiles = []

And in the control area:

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

And in ship:

    def fire_if_possible(self, missiles):
        if self.can_fire:
            missiles.append(Missile(self.mover.position, Vector2(0,0)))
            self.can_fire = False
            
    def not_firing(self):
        self.can_fire = True

And let’s be sure to move them and draw them:

    if ship.active: ship.draw(screen)
    for asteroid in asteroids:
        asteroid.draw(screen)
    for missile in missiles:
        missile.draw(screen)
    ...
    if ship.active: ship.mover.move(dt)
    for asteroid in asteroids:
        asteroid.mover.move(dt)
    for missile in missiles:
        missile.mover.move(dt)

And when we fly, we can leave little missiles behind:

row of missiles

Not bad, given that we just typed it in. Now what we should do, one imagines, is start them a bit in front of the ship, and give them a velocity of their own, plus that of the ship.

Let’s do a little testing here.

    def test_missile_start(self):
        ship = Ship(u.CENTER)
        ship.angle = 45
        pos = ship.missile_start()
        assert pos == Vector2(0, 0)

I’m not sure what I want the answer to be. Over in ship:

    def missile_start(self):
        radius = self.radius + 10
        return self.mover.position + Vector2(radius, 0).rotate(-self.angle)


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

So now that I know what I did, I know what I want. I sort of back into it:

    def test_missile_start(self):
        ship = Ship(Vector2(0, 0))
        ship.angle = 45
        pos = ship.missile_start()
        assert pos.x == pytest.approx(25, 0.5)
        assert pos.y == pytest.approx(-25, 0.5)

That works. Code is:

    def missile_start(self):
        radius = self.radius + 11
        offset = Vector2(radius, 0).rotate(-self.angle)
        return self.mover.position + offset

And it looks about right:

missile ring around ship

That test isn’t robust enough. Improve it:

    def test_missile_start(self):
        ship = Ship(Vector2(100, 100))
        ship.angle = 45
        pos = ship.missile_start()
        assert pos.x == pytest.approx(100+25, 0.5)
        assert pos.y == pytest.approx(100-25, 0.5)

OK, now the velocity. Let’s see what we did in Kotlin, just for reference. Hmm, missile speed is speed of light over three. Asteroid speed is 100 (for us and in Kotlin), and speed of light over there is 500. I’ll add it. I wind up with this:

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

    def not_firing(self):
        self.can_fire = True

    def missile_start(self):
        radius = self.radius + 11
        offset = Vector2(radius, 0).rotate(-self.angle)
        return self.mover.position + offset

    def missile_velocity(self):
        return Vector2(u.MISSILE_SPEED,0).rotate(-self.angle) + self.mover.velocity

It looks good on the screen, except that the dot could be larger. Fix that and I really ought to do a test. Oh, and they live forever, which is too long.

    def test_missile_velocity(self):
        ship = Ship(Vector2(100, 100))
        ship.angle = 45
        ship.mover.velocity = Vector2(500, 500)
        velocity = ship.missile_velocity()
        own_velocity = Vector2(u.MISSILE_SPEED, 0).rotate(-45)
        total_velocity = own_velocity + Vector2(500, 500)
        assert velocity == total_velocity

So that’s good enough. Now the timeout.

Test ...
    def test_missile_timeout(self):
        ship = Ship(Vector2(100,100))
        missiles = []
        ship.fire_if_possible(missiles)
        assert len(missiles) == 1
        missile = missiles[0]
        missile.update(missiles, 0.5)
        assert len(missiles) == 1
        missile.update(missiles, 3.0)
        assert len(missiles) == 0

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

Test runs. Put update call into main:

    if ship.active: ship.mover.move(dt)
    for asteroid in asteroids:
        asteroid.mover.move(dt)
    for missile in missiles.copy():
        missile.update(missiles, dt)
    for missile in missiles:
        missile.mover.move(dt)
    check_collisions()

I expect missiles to fly for 3 seconds and then die. They do. Let’s commit: missiles can be fired one at a time and fly for three seconds.

missiles fly

I feel the need to reflect. This is all working but I’m feel that it’s not really very well done.

Reflection

I’ve done just a few missile tests, and I did two of them after the fact. The code is fairly simple and rather clearly works, but I don’t really feel good about it all. Let’s review the whole life cycle:

#main ...
    if ship.active: ship.draw(screen)
    for asteroid in asteroids:
        asteroid.draw(screen)
    for missile in missiles:
        missile.draw(screen)

    keys = pygame.key.get_pressed()
    if keys[pygame.K_f]:
        ship.turn_left(dt)
    if keys[pygame.K_d]:
        ship.turn_right(dt)
    if keys[pygame.K_j]:
        ship.power_on(dt)
    else:
        ship.power_off()
    if keys[pygame.K_k]:
        ship.fire_if_possible(missiles)
    else:
        ship.not_firing()

    if ship.active: ship.mover.move(dt)
    for asteroid in asteroids:
        asteroid.mover.move(dt)
    for missile in missiles.copy():
        missile.update(missiles, dt)
    for missile in missiles:
        missile.mover.move(dt)
    check_collisions()

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

    def not_firing(self):
        self.can_fire = True

    def missile_start(self):
        radius = self.radius + 11
        offset = Vector2(radius, 0).rotate(-self.angle)
        return self.mover.position + offset

    def missile_velocity(self):
        return Vector2(u.MISSILE_SPEED,0).rotate(-self.angle) + self.mover.velocity

class Missile:
    def __init__(self, position, velocity):
        self.radius = 2
        self.mover = Mover(position, velocity)
        self.time = 0

    def draw(self, screen):
        pygame.draw.circle(screen, "white", self.mover.position, 4)

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

Well, maybe that’s not so bad. It might just be that working without tests, plus kind of feeling my way through the design, plus still not being up to speed on Python … maybe all that just adds up to a bit more discomfort than I like to feel.

We do have missiles, and they do fly, and they do time out. That’s good. We’ll make them do some damage … next time. For now, let’s have a break.

See you next time!