A bit of planning and a bit of doing.

By the way, if you read and enjoy these asteroids articles, please drop me a note or DM or something. Mostly I do them to entertain myself but if they’re enjoyed by others, it would be good to know. For extra credit, tell me why, or even what would be interesting to hear about. Thanks!

I thought I’d start the morning with some notes on things the program needs, and things I want to do even if the program doesn’t need them. One thing that I need is a centralized list of things that need doing. I mention ideas in the articles, but I don’t write them on a card or anything. I do find that ideas that matter tend to come up again, but it might be nice to have a list.

Here’s what comes up now, based on memory and looking around:

Ship Explosion Sound
When the ship encounters an asteroid, there is the asteroid’s sound, but when hit by the Saucer, nothing other than the visual effect we did yesterday. I’d like to add a ship explosion sound. My plan is to use an existing sound at a lower pitch.
Small Saucer Targeting
We don’t have a small saucer yet, but that’s a matter of a constant. However, the small saucer in the original game actually targeted the ship rather than firing randomly. It would be fun to implement that. I’m a bit inclined to work on that in a separate workspace.
Accessors
Various key aspects of the system are stored in the Universe as member variables, and there is a certain amount of direct access to values in other objects, such as pos and step. It’s usually considered better practice not to mess with other object’s variables directly. We might want to look at that, but it hasn’t been a problem so far.
Buttons
Buttons aren’t drawn or otherwise handled like other objects. But they are different in kind and access, so maybe that’s OK. We could take a look.
Score
Score is also not a drawn object, and it readily could be.
Scoring and Ships
In the real game, you get three or four ships (a dip switch setting), and when your last ship is destroyed, game over. We should do that. You also get a free ship every time your score rolls over 10,000. It seems only fair to do that as well.
Controls
I’ve been using the direct angular control wheel button for a while: the ship instantly points wherever your finger is on the circle. It’s different from the original game, where you had two buttons and a fixed rate of rotation, but it’s not bad. We might experiment with some alternatives. I’d be running it from the keyboard if that were possible, but the current Codea can’t access the keyboard much, if at all.
Control Positioning
I play with the iPad in landscape position, controls at the bottom. However, if we put the pad in portrait position, we could have a 1000x1000 screen and have the controls not obscure the screen at all. Might be worth trying.
Hyperspace
The original game had a “hyperspace” button. When you pressed it, your ship disappeared from where it was and reappeared a bit later at some random location. You could use hyperspace as an emergency escape when you were about to be destroyed. I believe that hyperspace was always safe, in the sense that you would always return alive, but I’m not certain of that. It was certainly possible to return too near to an asteroid and be destroyed that way. We should read the code a bit and then figure out how to do it.
Two Player Game
The original game could be played with two players taking turns. I don’t think we’ll do that, unless there is user demand for it.
Screen Scaling
I program and play on a 12.9 inch iPad Pro. There are smaller iPads, and I think Codea may actually work on the iPhone. We could adjust things based on screen scale. Here again, I see no user demand for this feature.

Today, however …

… is Saturday, so I think I’ll just do a couple of quick things and then turn to a day of relaxation and fun. This, as opposed to my other six days a week of relaxation and fun.

Let’s do a ship explosion sound. My plan for that is to use one of the existing explosions, probably the big asteroid, and pitch it lower. To experiment with that, I wrote this tiny program:

-- explode

function setup()
    s = asset.documents.Dropbox.bangLargeHi
    parameter.number("Volume", 0, 1, 1)
    parameter.number("Pitch", 0, 1, 1)
    parameter.action("Play", play)
end

function play()
    sound(s,Volume, Pitch)
end

That provides a couple of sliders and a Play button:

explosion

I decided that a pitch of 0.8 should do the job. I noticed that Codea changes the pitch by slowing down the sound, so it plays longer the lower you go.

Naturally, the explosion should be in stereo, and that means I want to add a pitch parameter to playStereo, which currently looks like this:

function Universe:playStereo(aSound, anObject)
    sound(aSound, 1, 1, 2*anObject.pos.x/WIDTH - 1)
end

I guess I’ll just append an optional pitch. I don’t love the calling sequence in that form but I think it’ll be OK.

function Universe:playStereo(aSound, anObject, optionalPitch)
    sound(aSound, 1, optionalPitch or 1, 2*anObject.pos.x/WIDTH - 1)
end

And put it into the Ship, after more typos than I care to admit:

function Ship:die()
    local f = function()
        Ship()
    end
    U:playStereo(U.sounds.bangLarge, self, 0.8)
    Explosion(self)
    U:deleteObject(self)
    Instance = nil
    tween(6, self, {}, tween.easing.linear, f)
end

It doesn’t add much unless you’re hit by a bullet, which so far is rare. But it was easy. Sometimes you just do what the Customer asks for, even if it’s dumb.

Score?

Let’s see about converting scoring so that it is drawn like the other indestructibles. Right now there’s a function score on every destructible and that function’s job is to add the appropriate value to U.score. Here’s the asteroid version of that:

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

This is pretty atrocious, although it works well. But here are all these objects all over, jamming values into U’s member variable, willy-nilly, slapdash, catch as catch can. You know what I mean.

The function score is itself called only twice, in Destructible:

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

It is also called in a test. We’ll surely want to fix that.

The score is drawn by the Universe:

function Universe:drawScore()
    local s= "000000"..tostring(self.score)
    s = string.sub(s,-5)
    pushStyle()
    fontSize(100)
    text(s, 200, HEIGHT-60)
    popStyle()
end

And that function is called here:

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()
    self:drawScore()
    self:findCollisions()
end

We can build a new class, Score, make it one of our singletons, put it in the indestructibles collection and draw it with everything else. Universe won’t even have to know there is such a thing.

I imagine it’ll have a method score or addScore, and perhaps little else.

The tricky bit will be that the score methods on all the destructibles will need to return the proposed score, and we’ll put that into the Score instance up in Destructible. I think we might want to test drive this.

What’s that test that calls score now?

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

Well, this is nearly good. Let’s see what that should look like with our new singleton Score. We have our own fresh universe when the test runs, so let’s permit ourselves to view the internals of Score even though no one else can.

How’s this for a start?

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

That should fail usefully for a while.

Hm, this is embarrassing. Some of the tests are failing. I was sure I had run them. Let’s see what’s up:

Well one issue, I just hadn’t run them since I did the sounds thing. Fake Universe can’t handle sounds stuff, and the ship is trying to access it.

Another is that there are a few tests that are concerned with score, so they’re all failing and need rewriting. This will happen when you change an interface as drastically as we’re doing here, but it is somewhat irritating. Let’s look at all of them:

        _:test("Asteroids increment score", function()
            local a = Asteroid()
            local m = Missile()
            local SC = Score:instance()
            _:expect(SC.score).is(0)
            a:score(m)
            _:expect(SC.score).is(20)
        end)
        
        _: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)
        
        _: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)

The first one is converted. I’m going to ignore the latter two, make the first one work, then unignore and fix the others. No sense changing them until I’m sure the interface is the way I want it.

Now to deal with the other failures:

22: saucer missiles kill ships -- Ship:102: attempt to index a nil value (field 'sounds')
        _: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)

Our Fake Universe can’t cope with requests for sounds. It’s not germane to what we’re doing here. Does kind of suggest that sounds should be a separate thing, but that’s not the swamp we’re here to drain. What happens if we copy the sounds from the real universe when we create the fake one? I bet we then explode on playStereo and we can just make a null one of those.

function FakeUniverse:init()
    self.sounds = U.sounds -- U is present. See before().
    self.currentTime = ElapsedTime
    self.score = 0
    self.destroyed = {}
end

That’s somewhat evil but all’s fair in fake objects.

22: saucer missiles kill ships -- Ship:102: attempt to call a nil value (method 'playStereo')

As expected. We counter:

function FakeUniverse:playStereo()
end
22: saucer missiles kill ships -- Fragment:16: attempt to call a nil value (method 'addIndestructible')

I am a terrible person. Obviously this hasn’t been run since I added indestructibles. I’m going to fix that habit today but for now we need to make this work. I think we just write a null method again.

function FakeUniverse:addIndestructible(ignored)
end
20: explosions don't collide -- Destructible:9: attempt to call a nil value (method 'mutuallyDestroy')
        _:test("explosions don't collide", function()
            local pos = vec2(200,200)
            U = FakeUniverse()
            x = Explosion(pos)
            m = Missile(pos)
            m:collide(x)
            _:expect(U:destroyedCount()).is(0)
            x:collide(m)
            _:expect(U:destroyedCount()).is(0)
        end)

This test is obsolete. Should have been removed yesterday. Explosions are indestructible and will not be collided by their nature. Remove this test.

13: Explosion added to objects -- Actual: 1, Expected: 2
        _:test("Explosion added to objects", function()
            _:expect(countObjects()).is(0)
            Ship()
            U:applyAdditions()
            _:expect(countObjects()).is(1)
            Explosion(Ship:instance())
            U:applyAdditions()
            _:expect(countObjects()).is(2)
        end)

This is also obsolete. Removing it.

We will fix this deplorable lack of reliance on the tests.

21: Asteroids increment score -- TestAsteroids:201: attempt to index a nil value (global 'Score')

Yes, at last. Now we need our Score class.

Score = class()

local Instance = nil

function Score:init()
    self.totalScore = 0
    Instance = self
end

function Score:instance()
    return Instance
end

function Score:draw()
end
21: Asteroids increment score -- TestAsteroids:202: attempt to index a nil value (local 'SC')
        _:test("Asteroids increment score", function()
            local a = Asteroid()
            local m = Missile()
            local SC = Score:instance()
            _:expect(SC.score).is(0)
            a:score(m)
            _:expect(SC.score).is(20)
        end)

This of course means that no one has instantiated a Score. That is the job of Universe and we are reminded that we want to remove the score variable from Universe, and we enhance this test:

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

That should ensure that no one is still setting anything in there, but only if we write more tests. Anyway, now in Universe:

function Universe:init()
    self.processorRatio = 1.0
    self.score = 0
    self.rotationStep = math.rad(1.5) -- degrees
...

That reference can be removed, but I think that Score should be created in startGame. I’ll put it there. But we have a fake Universe and we have to create our own Score there.

function Universe:startGame(currentTime)
    Score()
    self.currentTime = currentTime
    self.saucerTime = currentTime
    self.attractMode = false
    self.objects = {}
    self.indestructibles = {}
    createButtons()
    Ship()
    self.waveSize = nil
    self.lastBeatTime = self.currentTime
    self:newWave()
end


function FakeUniverse:init()
    Score()
    self.sounds = U.sounds -- U is present. See before().
    self.currentTime = ElapsedTime
    self.score = 0
    self.destroyed = {}
end

The errors now are:

21: Asteroids increment score -- Actual: nil, Expected: 0
21: Asteroids increment score -- Asteroid:31: attempt to perform arithmetic on a nil value (field 'score')

Our test, again is:

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

I decided when I implemented Score to save the score in a new name, totalScore. We could allow our test to access that variable, or we could demand an accessor method. I think that’s preferable from a design standpoint.

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

function Score:score()
    return totalScore
end

Oddly enough, that doesn’t work. I still get nit back from SC:score(). Oh:

function Score:score()
    return self.totalScore
end

Now I get the error I expected:

21: Asteroids increment score -- Asteroid:31: attempt to perform arithmetic on a nil value (field 'score')

This is the asteroid running its old score method.

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

And that’s what we were driving toward. We’ll fix that:

function Asteroid:score(anObject)
    if anObject:is_a(Saucer) then return end
    local s = self.scale
    if s == 16 then return 20
    elseif s == 8 then return 50
    else return 100
    end
end

And we need to use the new score approach in Destructible:

function Destructible:mutuallyDestroy(anObject)
    if self:inRange(anObject) then
        local SC = Score:instance()
        SC:addScore(self:score(anObject))
        SC:addScore(anObject:score(self))
        self:die()
        anObject:die()
    end
end

And …

function Score:addScore(aNumber)
    self.totalScore = self.totalScore + aNumber
end

Now I really do expect this test to run. However, it does not:

21: Asteroids increment score -- Actual: 0, Expected: 20

What is this test again?

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

Hmm. Oh. We didn’t do what we expected Destructible to do, namely put the score into SC. Our test needs to be:

21: Asteroids increment score -- OK

Of course some other tests are failing because the score functions being called are not correct. And we have our other two tests, which need to be converted and unignored:

        _: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(Score:instance():score()).is(0)
            U.destroyed = {}
            a:collide(m)
            _:expect(U:destroyedCount()).is(0)
            _:expect(U.score).is(0)
        end)

I nearly expect this one to work. And it does. The other ignored test:

        _:ignore("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(SC:instance():score()).is(0)
            U.destroyed = {}
            a:collide(s)
            _:expect(U:destroyedCount()).is(2)
            _:expect(SC:instance():score()).is(0)
        end)

This works as well. Other tests are failing now, because other objects are accessing U.score as a number and it’s not there any more.

20: saucer missiles kill ships -- Score:15: attempt to perform arithmetic on a nil value (local 'aNumber')
        _: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)

Nothing wrong with this test, it’s just that the score methods for saucer missile and ship do not conform.

function Ship:score()
    return 0
end

function Missile:score()
    return 0
end

SaucerMissile inherits score from Missile so this should be good.

17: saucer vs asteroid both ways -- Score:15: attempt to perform arithmetic on a nil value (local 'aNumber')

This will be Saucer:score:

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

Becomes:

function Saucer:score(anObject)
    if anObject:is_a(Missile) then
        return 250
    end
end
17: saucer vs asteroid both ways -- Score:15: attempt to perform arithmetic on a nil value (local 'aNumber')

That’s the same error, let’s look a bit deeper:

        _:test("saucer vs asteroid both ways", function()
            local pos = vec2(200,200)
            U = FakeUniverse()
            s = Saucer(pos)
            a = Asteroid(pos)
            s:collide(a)
            _:expect(U:destroyedCount()).is(2)
            U.destroyed = {}
            a:collide(s)
            _:expect(U:destroyedCount()).is(2)
        end)

Ah:

function Asteroid:score(anObject)
    if anObject:is_a(Saucer) then return end
    local s = self.scale
    if s == 16 then return 20
    elseif s == 8 then return 50
    else return 100
    end
end

That guard clause should return 0 now, not just return.

And also:

function Saucer:score(anObject)
    if anObject:is_a(Missile) then
        return 250
    end
end

This needs to return 0, not nothing:

function Saucer:score(anObject)
    if anObject:is_a(Missile) then
        return 250
    else
        return 0
    end
end

Unignore the following and fix the reference to Score:

        _: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(Score:instance():score()).is(0)
            U.destroyed = {}
            a:collide(s)
            _:expect(U:destroyedCount()).is(2)
            _:expect(Score:instance():score()).is(0)
        end)
23 Passed, 0 Ignored, 0 Failed

This took longer than I anticipated, but there were only brief moments of confusion and mostly I just let the tests tell me what to do. Now I’ll use Codea’s find to see if there are any implementations of score() that son’t conform. If there are, they are evidence of a missing test.

There are none. I’ll commit: “score internals work’.

We are left with the need for the score to display. Presently our Score instance has no draw method, and in Universe, drawScore is now drawing “00nil”, which is interesting if not useful. So:

function Score:init()
    self.totalScore = 0
    Instance = self
    U:addIndestructible(self)
end

function Score:draw()
    local s= "000000"..tostring(self.totalScore)
    s = string.sub(s,-5)
    pushStyle()
    fontSize(100)
    text(s, 200, HEIGHT-60)
    popStyle()
end

And it doesn’t show up. Because:

function Universe:startGame(currentTime)
    Score()
    self.currentTime = currentTime
    self.saucerTime = currentTime
    self.attractMode = false
    self.objects = {}
    self.indestructibles = {}
    createButtons()
    Ship()
    self.waveSize = nil
    self.lastBeatTime = self.currentTime
    self:newWave()
end

Might be best to create it after we initialize the objects. I’ll move all the creations down closer to the bottom and that might help avoid other issues.

I quickly discover that Score needs a move method:

function Score:move()
end

It doesn’t move much. I’m not sure what kind of test might have found that. Anyway, with that change the tests all run and the game plays properly.

That’ll do for this morning. I’ll do a brief retrospective, as usual.

Summing Up

Commit: “score object works”.

I think the big lesson here is that the tests really drove out nearly all the needed behavior for converting scoring into an object, with the exceptions being getting it to draw and (not) move.

Another lesson is that I’ve still not built up the habit of running them as often as I should, especially when I think I’m done for the session, I’ve failed (pardon the expression) to run them at the very end of the day.

I’ll look into running them automatically, but that will have to be done with some care, not least because any users (and there are a couple) shoudn’t be forced to load up CodeaUnit if they don’t want to.

I’ll explore that in my copious free time, though. It should be easy enough, since I do have my own copy of the CodeaUnit source.

I think the next interesting thing will be accurate targeting. I’ve found an article on exactly how to do it, basically solving a quadratic. I can tell that Asteroids isn’t doing that, and I’ll see if I can figure out what it is doing, and maybe also see if I can devise a simpler way that works fairly well. We can afford the quadratic, of course, even though the original couldn’t.

Anyway, that’s it for this morning. See you next time, and if you’re out there, please let me know.

Zipped Code