Some sound, and saucer needs to stop helping so much.

With collisions well in hand, it’s time to mess them up a bit. The saucer just comes swinging in and scoring points on our behalf. As best as I can determine from watching games, the saucer’s bullets do not destroy asteroids, and when it crashes into an asteroid, the asteroid splits, the saucer dies, and neither the asteroid nor the saucer adds to the user’s score.

There are also saucer sounds. We have the saucer’s firing noise, but it also makes a shrill kind of scree noise, just like real flying saucers. The interesting thing about that sound is that for best results, it needs to be looped.

We’re only supporting the big saucer for now, and I’ll try not to put in small saucer features before they’re needed, tempting though it is.

Oh, an interesting feature of the small saucer, that makes me want to work on it is that it fires one random shot, and after that its shots are targeted. From the 6502 code, it appears that it even considers the relative velocities of the saucer and ship. That’ll be fun.

But for now, big saucer.

The additional sound needs to play, looped, while the saucer is on screen, and really needs to stop when it isn’t. I say really because it’s quite loud and rather irritating. Here’s our relevant code:

function Saucer:init(optionalPos)
    function die()
        if self == Instance then
            self:dieQuietly()
        end
    end
    Instance = self
    U:addObject(self)
    self.pos = optionalPos or vec2(0, math.random(HEIGHT))
    self.step = vec2(2,0)
    self.fireTime = U.currentTime + 1 -- one second from now
    if math.random(2) == 1 then self.step = -self.step end
    tween(7, self, {}, tween.easing.linear, die)
end

function Saucer:die()
    Splat(self.pos)
    self:dieQuietly()
end

function Saucer:dieQuietly()
    U:deleteObject(self)
    Instance = nil
    U.saucerTime = U.currentTime
end

Saucer has those two “die” methods because it can die from a hit, which is the die method, or it can time out, which is dieQuietly. The former just drops a splat to mark the occasion, and the latter does all the work. So we’ll set the sound in init, and stop it in dieQuietly:

I’ll insert this into init:

    self.sound = sound(asset.saucerBigHi, 0.8, 1, 0, true)

The parameters there are sound name, volume, pitch, pan, and loop. I’ve set the volume down to 0.8 in aid of peace in the home. Now we need to be sure to stop the loop, so I’ll put that in dieQuietly:

function Saucer:dieQuietly()
    self.sound:stop()
    U:deleteObject(self)
    Instance = nil
    U.saucerTime = U.currentTime
end

This undocumented sound function does the job. Now I should have a lovely scree when the saucer shows up. And I do.

Should I have test-driven that? I didn’t, because I’d played with it and knew how to do it, and besides, it’s trivially easy. Worse, there’s no real way to know that the sound isn’t playing. We could purposely set another flag or nil a pointer or something, but that would make little sense. As far as I can tell, there’s no decent way to test this with a microtest.

Now you could argue that sounds should be controlled either in Universe or a separate sound controller, and if that were the case we could mock that aspect and see that the sounds-on and sounds-off messages were sent. We could, but that’s not where we’re at.

Where we’re at, however, we do have an asteroid scree sound that turns on and off appropriately.

Saucer Missiles

Now that our fingers are warmed up, we need to do something about the saucer missiles. They should kill the ship, and they should not kill asteroids.

That’s absolutely going to take us outside our current design, with objects either participating in mutual destruction, or being indestructible, like Explosions. Speaking of Explosions, we should do something nicer than “BLAMMO” with those one of these days.

Also speaking of Explosions, we have Splats, which are nicely animated and don’t participate in collisions because they are in a separate collection and drawn separately. We could generalize that for Explosions when the time comes. Try to remember that.

Anyway saucer missiles are special. Right now, there are two little factory methods for firing:

function Missile:init(pos, step)
    function die()
        self:die()
    end
    self.pos = pos
    self.step = step 
    U:addObject(self)
    tween(3, self, {}, tween.easing.linear, die)
end

function Missile:fromShip(ship)
    local pos = ship.pos + vec2(ship:killDist() + 1,0):rotate(ship.radians)
    local step = U.missileVelocity:rotate(ship.radians) + ship.step
    return Missile(pos, step)
end

function Missile:fromSaucer(saucer)
    local rot = math.random()*2*math.pi
    local pos = saucer.pos + vec2(saucer:killDist() + 1, 0):rotate(rot)
    local vel = U.missileVelocity:rotate(rot) + saucer.step
    return Missile(pos, vel)
end

I suppose there are a number of choices for how to handle these new missiles, but given our current design, and the general notions of object-oriented design, they should be a new object, of a new class. I nominate the name SaucerMissile. Now here is where I’m going to pull another Dave Rooney here …

“Let’s make the mistake of …” subclassing SaucerMissile from Missile. It’s certainly a “kind of” missile, so it may work out OK.

That said, subclassing from concrete classes has come into disregard in the OO community, so let’s be alert for trouble.

First, let’s fix Saucer’s request to fire a missile, which reads:

function Saucer:move()
    U:moveObject(self)
    if U.currentTime >= self.fireTime then
        U:playStereo(U.sounds.saucerFire, self)
        self.fireTime = U.currentTime + 1
        Missile:fromSaucer(self)
    end
end

I’ll change that to request SaucerMissile. Now what? What about some tests? We know that this guy has collision behavior unlike any seen before. Let’s belay the notion of subclassing him and see where our tests can guide us.

We know how to test collisions, and we have perfectly good code for moving and such, so let’s just dive in and try some microtests.

        _:test("saucer missiles kill ships", function()
            local pos = vec2(123,456)
            local s = Ship(pos)
            local m = SaucerMissile(pos)
            s:collide(m)
            _:expect(U:destroyedCount()).is(2)
        end)

This fails initially because there is no SaucerMissile so we’ll create one:

SaucerMissile = class()

function SaucerMissile:init(pos)
    self.pos = pos
end

I went wild there and initialized pos. We’ll steal the init from regular missiles one way or another, when the time comes. Now the test says:

23: saucer missiles kill ships -- Destructible:9: 
attempt to call a nil value (method 'mutuallyDestroy')

Well, I’m wondering how that happened. Oh. I forgot to put this all into a fake universe. So stuff happened.

        _:test("saucer missiles kill ships", function()
            local pos = vec2(123,456)
            U = FakeUniverse()
            local s = Ship(pos)
            local m = SaucerMissile(pos)
            m:collide(s)
            _:expect(U:destroyedCount()).is(2)
        end)

I also reversed the sense of the collide because I want to drive out collide first. The message now is:

23: saucer missiles kill ships -- TestAsteroids:218: 
attempt to call a nil value (method 'collide')

That’s more like what I wanted. An interesting side effect of putting sound into the saucer is that now the tests start that irritating noise and it does’t stop. I’ll not divert to fix that just now. I’m on a mission.

OK, what does a saucer missile do about collide? It wants to ignore all collisions except with ships. There is really only one way to do this that I can think of: we’ll have to interrogate the type. (There are roundabout ways, and our implement n-squared methods way would have sorted it out, and there is a whole ‘nother way that we’ll talk about later if at all, but for now, I’ll make the mistake of … interrogating type:

No, wait. We don’t have a test for that. What’s coming through is a ship and we allow that, so we should just do what Destructible does:

function Destructible:collide(anObject)
    if self:unrelated(anObject) then
        anObject:mutuallyDestroy(self)
    end
end

And we don’t have a test for the unrelated issue either, and we’re not going to need it, maybe? so we’ll just do this:

function SaucerMissile:collide(anObject)
    anObject:mutuallyDestroy(self)
end

This is one of the hardest aspects of the microtesting or TDD discipline. We try to write only the necessary code to make the test pass. And doing so leads to a very smooth rhythm of test-code-test-code, that’s quite sustainable and pleasant. But when we’re in the habit of writing code that we “know” we’re going to need, we put in code that may not be necessary, and certainly isn’t tested.

Since the microtests have only been slightly helpful here in Asteroids, my habits are to code what I “know”, not what the test asks for. So hold me back if you see me coding more than I need.

Running the test again we see:

23: saucer missiles kill ships -- Destructible:28: 
attempt to call a nil value (method 'killDist')

That surprised me, though perhaps it shouldn’t have. I was expecting to see the mutual destruction message but of course we need a kill distance. Missiles have zero radius if I recall. I verify that on Missile and then write:

function SaucerMissile:killDist()
    return 0
end

The test now says:

23: saucer missiles kill ships -- Destructible:16: 
attempt to call a nil value (method 'score')

This is rather neat. The tests have nicely led me through a series of implementations that I need, and was not anticipating. Had I just done something and run it in the game, the game would surely have crashed.

Missiles don’t score:

function SaucerMissile:score()
end

The test now says:

23: saucer missiles kill ships -- Destructible:18: 
attempt to call a nil value (method 'die')

Hm. Time to die. OK, we can do that:

function SaucerMissile:die()
    U:deleteObject(self)
end

And the test says:

23: saucer missiles kill ships -- OK

Let’s reverse the sense of the collision and see what happens.

By the way, I am totally getting into the groove of this test and driving out the behavior. I’m not looking forward as much as before, not juggling thoughts in my head. I’m confident that I can and will write enough tests to get this right.

Very different from how I usually feel writing this program, and I prefer feeling like this. I add three lines to the end of our test:

        _:test("saucer missiles kill ships", function()
            local pos = vec2(123,456)
            U = FakeUniverse()
            local s = Ship(pos)
            local m = SaucerMissile(pos)
            m:collide(s)
            _:expect(U:destroyedCount()).is(2)
            U.destroyed = {}
            s:collide(m)
            _:expect(U:destroyedCount()).is(2)
        end)

And the test says:

23: saucer missiles kill ships -- Destructible:9: 
attempt to call a nil value (method 'mutuallyDestroy')

The standard version of that method is in Destructible:

function Destructible:mutuallyDestroy(anObject)
    if self:inRange(anObject) then
        self:score()
        anObject:score()
        self:die()
        anObject:die()
    end
end

If we were inheriting from Destructible, this all might have been easier. Similarly for inheriting from Missile.

I’m really inclined to inherit from one or the other. But I shouldn’t really change direction with a failing test. Let’s proceed as we are.

The SaucerMissile version of mutuallyDestroy should check inRange, but it shouldn’t score, because we don’t want people getting points if the saucer destroys things. Plus, we’re on a mission to create a missile that doesn’t destroy things. So let’s do this:

function SaucerMissile:mutuallyDestroy(anObject)
    if self:inRange(anObject) then
        self:die()
        anObject:die()
    end
end

function SaucerMissile:inRange(anObject)
    local triggerDistance = self:killDist() + anObject:killDist()
    local trueDistance = self.pos:dist(anObject.pos)
    return trueDistance < triggerDistance
end

We’re duplicating a lot of code and it’s starting to bug me. But we have a capability to create and our discipline is to make it work, then make it right. So now the test says:

23: saucer missiles kill ships -- OK

Sweet. We could, in principle, refactor now to remove that duplication. But I think it’s unwise because we haven’t really driven out the hard part of this behavior. We could have made SaucerMissile a destructible and wound up just about here.

Let’s make a SaucerMissile not kill an asteroid:

        _:test("saucer missiles don't kill asteroids", function()
            local pos = vec2(111,222)
            U = FakeUniverse()
            local a = Asteroid(pos)
            local m = SaucerMissile(pos)
            m:collide(a)
            _:expect(U:destroyedCount())is(0)
        end)

Hmm. The error is:

24: saucer missiles don't kill asteroids -- Asteroid:30: 
attempt to perform arithmetic on a nil value (field 'score')

Asteroid is trying to score itself. Which makes me happy because I don’t want that. FakeUniverse doesn’t have score, so we’ll improve that:

function FakeUniverse:init()
    self.currentTime = ElapsedTime
    self.score = 0
    self.destroyed = {}
end

And I know something else I’d like to test in this test: the score. Normally I’d wait, but I don’t want to forget:

        _:test("saucer missiles don't kill asteroids", function()
            local pos = vec2(111,222)
            U = FakeUniverse()
            local a = Asteroid(pos)
            local m = SaucerMissile(pos)
            m:collide(a)
            _:expect(U:destroyedCount()).is(0)
            _:expect(U.score).is(0)
        end)

And the test says …

24: saucer missiles don't kill asteroids -- 
Actual: 2, Expected: 0
24: saucer missiles don't kill asteroids -- 
Actual: 20, Expected: 0

Both the destroyed count and the score failed. We’ll probably want to check score as we go on, because when the saucer collides with an asteroid, it shouldn’t score and I believe that it does.

OK, what has happened. We had the missile collide with the asteroid, so we got first look at the situation. We should have never sent mutuallyDestroy to the asteroid. Once we did, it was too late.

Our missile only destroys ships. We need to say that now:

function SaucerMissile:collide(anObject)
    if anObject:is_a(Ship) then
        anObject:mutuallyDestroy(self)
    end
end

I see no better way to do this than just to determine whether we, the saucer missile, have hit a ship, and if so, destroy it. I hate checking the class but it seems to be the best we can do. The alternative would be for the ship to know that it is invulnerable to saucer missiles, and that seems worse.

Now the test says:

24: saucer missiles don't kill asteroids -- OK

So we’ll do it in the other direction:

        _:test("saucer missiles don't kill asteroids", function()
            local pos = vec2(111,222)
            U = FakeUniverse()
            local a = Asteroid(pos)
            local m = SaucerMissile(pos)
            m:collide(a)
            _:expect(U:destroyedCount()).is(0)
            _:expect(U.score).is(0)
            U.score = 0
            U.destroyed = {}
            a:collide(m)
            _:expect(U:destroyedCount()).is(0)
            _:expect(U.score).is(0)
        end)
24: saucer missiles don't kill asteroids -- 
Actual: 2, Expected: 0

As expected, the opposite direction test faiils. Why? Because the saucer missile received mutuallyDestroy and it looks like this:

function SaucerMissile:mutuallyDestroy(anObject)
    if self:inRange(anObject) then
        self:die()
        anObject:die()
    end
end

We need to limit this method to ships as well:

function SaucerMissile:mutuallyDestroy(anObject)
    if anObject:is_a(Ship) then
        if self:inRange(anObject) then
            self:die()
            anObject:die()
        end
    end
end

I expect our test to run, and it does. Do we need more tests? I would say that we do not. We can think of other tests but since saucer bullets are now only interested in ships, I think we’ve covered ourselves pretty well. I’m going to do a little game play, though, to see if anything untoward happens. And I want to see it working.

Of course, since I was watching, it took forever for the saucer to hit me but ultimately it did. Everything seemed to work as expected …

With one exception. In one wave, the last asteroid on the screen was splittable. When I shot it, I could have sworn that the game triggered a new wave.

Remember that added objects are added after the collision cycle runs, and if I recall, it’s done at or near the top of the draw loop. If the checker for no asteroids were to check before that add was done, it would mistakenly think the wave was over. Let’s look:

function Universe:draw(currentTime)
    self:applyAdditions()
    self:adjustTimeValues(currentTime)
    --displayMode(FULLSCREEN_NO_BUTTONS)
    background(40, 40, 50)
    checkButtons()
    drawButtons()
    self:drawEverything()
    self:moveEverything()
    drawSplats()
    self:drawScore()
    self:findCollisions()
    self:checkBeat()
    self:checkSaucer()
    self:checkNewWave()
end

Sure enough, the check occurs at the end of one cycle, and the adding at the beginning of the next. I’ll move the check up after the add:

function Universe:draw(currentTime)
    self:applyAdditions()
    self:checkBeat()
    self:checkSaucer()
    self:checkNewWave()
    self:adjustTimeValues(currentTime)
    --displayMode(FULLSCREEN_NO_BUTTONS)
    background(40, 40, 50)
    checkButtons()
    drawButtons()
    self:drawEverything()
    self:moveEverything()
    drawSplats()
    self:drawScore()
    self:findCollisions()
end

I decided to move all the checks up. Should I move the adding to the end as well? Fact is, these two methods must be called in this order. How can we express that in the code?

There’s a lot going on there. We should probably break this method up, and perhaps even consider pushing more logic up into Main. That’s not for today, though.

I think the SaucerMissile works. Let’s look at it and see if it’s right.

SaucerMissile = class()

function SaucerMissile:init(pos)
    self.pos = pos
end

function SaucerMissile:collide(anObject)
    if anObject:is_a(Ship) then
        anObject:mutuallyDestroy(self)
    end
end

function SaucerMissile:killDist()
    return 0
end

function SaucerMissile:score()
end

function SaucerMissile:die()
    U:deleteObject(self)
end

function SaucerMissile:mutuallyDestroy(anObject)
    if anObject:is_a(Ship) then
        if self:inRange(anObject) then
            self:die()
            anObject:die()
        end
    end
end

function SaucerMissile:inRange(anObject)
    local triggerDistance = self:killDist() + anObject:killDist()
    local trueDistance = self.pos:dist(anObject.pos)
    return trueDistance < triggerDistance
end

The only thing that really bugs me there is the duplication of inRange, here and in Destructible. I don’t see a way to make that better right now. I’m calling this “right enough” and committing “saucer missiles only kill ships”.

For some reason Working Copy thought 8 files had changed. Either I forgot to commit something last night, or it got confused. Anyway, all committed now.

Now we do have another issue, which is that when the Saucer crashes into an asteroid, we get the score. Let’s see what we can do about that.

Saucer is a Destructible, so its collision logic is all up there:

function Destructible:collide(anObject)
    if self:unrelated(anObject) then
        anObject:mutuallyDestroy(self)
    end
end

function Destructible:mutuallyDestroy(anObject)
    if self:inRange(anObject) then
        self:score()
        anObject:score()
        self:die()
        anObject:die()
    end
end

We have a way to test scoring, so let’s just write a test:

        _:test("saucer-asteroid collisions do not score", function()
            local pos = vec2(222,333)
            U = FakeUniverse()
            local s = Saucer(pos)
            local a = Asteroid(pos)
            s:collide(a)
            _:expect(U:destroyedCount()).is(2)
            _:expect(U.score).is(0)
            U.destroyed = {}
            U.score = 0
            a:collide(s)
            _:expect(U:destroyedCount()).is(2)
            _:expect(U.score).is(0)
        end)

I wish you could feel how I feel with these tests beginning to work for me. I know just as little about the code as ever, my ignorance and forgetfulness are no lower, but I feel very confident and calm, because this test is going to help me be sure I’m getting just what I want. Let’s run it:

25: saucer-asteroid collisions do not score -- 
Actual: 270, Expected: 0

This occurred twice and both the count tests were OK. I expected that exact result.

Now I think we should leave the destruction logic alone: it’s destroying the right things. But the score logic in Saucer is this:

function Saucer:score()
    U.score = U.score + 250
end

That needs to happen only if the saucer is destroyed by a missile. But we don’t know who is scoring on us. What if we enhanced score to include an argument, the object that was on the other side of the collision? Then we could check it. It’ll be another of those is_s tests but that’s the best idea I have at present.

So I want this method:

function Saucer:score(anObject)
    if anObject:is_a(Missile) then
        U.score = U.score + 250
    end
end

Now we need all senders of score to pass along the other object:

function Destructible:mutuallyDestroy(anObject)
    if self:inRange(anObject) then
        self:score(anObject)
        anObject:score(self)
        self:die()
        anObject:die()
    end
end

Aside from one in the tests, those seem to be the only senders of score. Let’s run the tests:

25: saucer-asteroid collisions do not score -- 
Actual: 20, Expected: 0

That occurs twice. Unfortunately, we have to condition the asteroid not to score for saucer collisions. It’s already pretty complex:

function Asteroid:score()
    local s = self.scale
    local inc = 0
    if s == 16 then inc = 20
    elseif s == 8 then inc = 50
    else inc = 100
    end
    U.score = U.score + inc
end

I’m thinking Guard Clause here:

function Asteroid:score(anObject)
    if anObject:is_a(Saucer) then return end
    local s = self.scale
    local inc = 0
    if s == 16 then inc = 20
    elseif s == 8 then inc = 50
    else inc = 100
    end
    U.score = U.score + inc
end
25: saucer-asteroid collisions do not score -- OK

OK, that works and it’s as right as I know how to do. Let’s commit “saucers don’t help with scoring”.

Hold on there bunky. Playing the game, I definitely saw saucer missiles killing asteroids. What’s missing?

function Saucer:move()
    U:moveObject(self)
    if U.currentTime >= self.fireTime then
        U:playStereo(U.sounds.saucerFire, self)
        self.fireTime = U.currentTime + 0.5
        Missile:fromSaucer(self)
    end
end

The saucer isn’t firing the right kind of missile. And we should remove the fromSaucer method from Missile while we’re at it.

Can we beef up our tests to check this? Not readily, because firing is time dependent. We could refactor this to be more testable, but not today. I’ll just fix it. Sorry.

function Saucer:move()
    U:moveObject(self)
    if U.currentTime >= self.fireTime then
        U:playStereo(U.sounds.saucerFire, self)
        self.fireTime = U.currentTime + 0.5
        SaucerMissile:fromSaucer(self)
    end
end

We also need

function SaucerMissile:fromSaucer(saucer)
    local rot = math.random()*2*math.pi
    local pos = saucer.pos + vec2(saucer:killDist() + 1, 0):rotate(rot)
    local vel = U.missileVelocity:rotate(rot) + saucer.step
    return Missile(pos, vel)
end

And I had to run the program to rediscover that, despite the fact that I knew darn well it needed to be there.

AND … it still wasn’t right:

function SaucerMissile:fromSaucer(saucer)
    local rot = math.random()*2*math.pi
    local pos = saucer.pos + vec2(saucer:killDist() + 1, 0):rotate(rot)
    local vel = U.missileVelocity:rotate(rot) + saucer.step
    return SaucerMissile(pos, vel)
end

OMG! Now they don’t appear on the screen. That’s because a) they don’t register with the universe, and b) they don’t know how to draw and move. That code needs to come from Missile. I’ll copy/paste it in for now but we’ve gotta talk about this.

After pasting those methods in and editing them to the right class, it seems to work. But my bubble of confidence from the test has been burst. We did test killing and scoring and those do work. We didn’t test whether saucers fire saucer missiles and whether those missiles move and show up on the screen. Had this been something more subtle, we could have gone a long time without discovering the errors.

We also have substantial duplication now between Missile and SaucerMissile. I believe that init, draw, move, killDist and die are identical between the two classes, and only collide and mutuallyDestroy are different, plus we had to copy and add inRange. This class is like 80 percent identical to Missile, maybe more.

The “easy” “fix” is to make SaucerMissile inherit from Missile, and override just the methods we need to change. That will absolutely work (i.e. if it doesn’t work instantly, it can be made to work). But the people who tell me things tell me that inheriting from concrete classes is “bad”.

Before I forget, let’s commit this code: “Saucer Missile working”. Now when I make a mistake real soon now, I’ve not lost the good parts of this work.

A less easy fix would be to pull some or all of the duplicate methods out to another object or objects that support missile activities and use those objects in both Missile and SaucerMissile. That doesn’t seem tasty to me at all.

Here we’ll finally make the mistake of <dramatic music> subclassing. I’m going to make SaucerMissile inherit from Missile, remove all the duplicate stuff, and we’ll see if we get in trouble later.

This seems to work. I did have to move the SaucerMissiles tab to the right of the Missiles tab, so that the compiler pics up the new global Missile, but with that in place, SaucerMissile looks like this:

SaucerMissile = class(Missile)

function SaucerMissile:collide(anObject)
    if anObject:is_a(Ship) then
        anObject:mutuallyDestroy(self)
    end
end

function SaucerMissile:mutuallyDestroy(anObject)
    if anObject:is_a(Ship) then
        if self:inRange(anObject) then
            self:die()
            anObject:die()
        end
    end
end

There’s one test failing, and it’s this one:

        _:test("Asteroids increment score", function()
            local a = Asteroid()
            U.score = 0
            a:score()
            _:expect(U.score).is(20)
        end)

That’s because of our new convention that we pass along the destructor when we score. We’ll fix the test:

        _:test("Asteroids increment score", function()
            local a = Asteroid()
            local m = Missile()
            U.score = 0
            a:score(m)
            _:expect(U.score).is(20)
        end)

All the tests pass. Commit: “saucermissile inherits from missile”.

Now to sum up. But first, a break. BRB.

Summing Up

Inheritance

Well, first of all, that inheritance. Aside from received wisdom about not using inheritance this way, I like the results. I’ll include the entire SaucerMissile hierarchy here, in the hope that some readers will tell me why this is bad and what would be less bad.

Destructible = class()

function Destructible:init(x)
    assert(false) -- no one should make one of these
end

function Destructible:collide(anObject)
    if self:unrelated(anObject) then
        anObject:mutuallyDestroy(self)
    end
end

function Destructible:mutuallyDestroy(anObject)
    if self:inRange(anObject) then
        self:score(anObject)
        anObject:score(self)
        self:die()
        anObject:die()
    end
end

function Destructible:score()
    dump("score", self)
    assert(false)
end

function Destructible:inRange(anObject)
    local triggerDistance = self:killDist() + anObject:killDist()
    local trueDistance = self.pos:dist(anObject.pos)
    return trueDistance < triggerDistance
end

function Destructible:unrelated(anObject)
    return getmetatable(self) ~= getmetatable(anObject)
end

function dump(msg, p,q)
    print(msg)
    if p then dumpObj(p) end
    if q then dumpObj(q) end
end

function dumpObj(o)
    print(kind(o), o, o.pos)
end

function kind(o)
    if o:is_a(Missile) then return "missile" end
    if o:is_a(Saucer) then return "saucer" end
    if o:is_a(Ship) then return "ship" end
    if o:is_a(Asteroid) then return "asteroid" end
    return "unknown"
end

-- Missile
-- RJ 20200522

Missile = class(Destructible)

function Missile:init(pos, step)
    function die()
        self:die()
    end
    self.pos = pos
    self.step = step 
    U:addObject(self)
    tween(3, self, {}, tween.easing.linear, die)
end

function Missile:fromShip(ship)
    local pos = ship.pos + vec2(ship:killDist() + 1,0):rotate(ship.radians)
    local step = U.missileVelocity:rotate(ship.radians) + ship.step
    return Missile(pos, step)
end

function Missile:die()
    U:deleteObject(self)
end

function Missile:score()
end

function Missile:draw()
    pushStyle()
    pushMatrix()
    fill(255)
    stroke(255)
    ellipse(self.pos.x, self.pos.y, 6)
    popMatrix()
    popStyle()
end

function Missile:move()
    U:moveObject(self)
end

function Missile:killDist()
    return 0
end

SaucerMissile = class(Missile)

function SaucerMissile:collide(anObject)
    if anObject:is_a(Ship) then
        anObject:mutuallyDestroy(self)
    end
end

function SaucerMissile:mutuallyDestroy(anObject)
    if anObject:is_a(Ship) then
        if self:inRange(anObject) then
            self:die()
            anObject:die()
        end
    end
end

Tests

The tests really helped me. By the time I had completed the destruction and scoring tests, I was quite sure that the new changes worked properly. Working with them went smoothly and the tests just gently moved me forward in the implementation. That was great, and just the way I’ve found it to be when the tests are solid.

And then there was that minor detail that I never installed the new SaucerBullets so that they were used in the game, and nearly didn’t notice. There are no tests of that kind, and right there before our eyes we saw why we miss them.

In the first instance, the tests gave us solid and deserved confidence in the program. Right after that, the lack of tests nearly allowed a defect to escape, and required manual game-play testing to notice the defect at all.

There’s a lesson there … if I could just think what it might be …

Overall

Overall, things went rather well, and we’re a bit closer to replicating Asteroids to our satisfaction. The small saucer will be interesting, especially giving it accurate shots, and then there’s hyperspace, and maybe a limited number of ships.

Oh, and how about a nice ship explosion? I have a treat in store for that. Maybe I’ll do that next.

Stop by and see. And if you want to code up a better way of handling destruction and scoring, do let me know. Even if you’re just guessing what might be better, we can have a little Twitter chat about it.

See you then!

Zipped Code