Run tests automatically, dribs, drabs, and oddities. Pretty calm.

I made a tweak to CodeaUnit to save feature name and run summary to a string and return it from execute. Ran into an oddity with Working Copy: I had done the CodeaUnit change on my old iPad, and sent the zipped CodeaUnit to this one. Renamed the old CodeaUnit to C2deaUnit, and unzipped. Working Copy was still looking at the old file, despite the rename. Nothing I thought of could sort it, so I pasted the new code into the old file, deleted, renamed, and now all’s good. I may have to start using remotes for CodeaUnit.

I’ll include a copy of the current CodeaUnit in this article.

With that summary info being saved accessibly, I modified Asteroids to run the tests automatically and display the results on the startup screen:

local Console = ""

function setup()
    runTests()
    U = Universe()
    U:newWave()
end

function runTests()
    if not CodeaUnit then return end
    local det = CodeaUnit.detailed
    CodeaUnit.detailed = false
    Console = _.execute()
    CodeaUnit.detailed = det
end

function draw()
    U:draw(ElapsedTime)
    if U.attractMode then
        pushStyle()
        fontSize(50)
        fill(255,255,255, 128)
        text("TOUCH SCREEN TO START", WIDTH/2, HEIGHT/4)
        text(Console, WIDTH/2, HEIGHT - 200)
        --print("//",Console)
        popStyle()
    end
end

And the result looks like this:

startup screen shows test summary

I may have to make it display in red and make a rude noise if the tests fail, to be sure that I notice. Not that I don’t trust myself.

Making this change drove out an interesting few problems. One was that the saucer screaming noise was triggered multiple times and didn’t stop when the tests ran. That was because the saucers were just discarded, not passing die, not turning off their sounds. So I modified all the saucer tests to send die to the saucers.

That was tedious, and I suspect that ideally I’d have made a new feature about the saucers, and done the saucer creation and teardown in the before and after functions. Just killing them will do for now.

I’ve encountered another interesting issue though. Recall that ships automatically respawn, due to this code:

function Ship:die()
    local f = function()
        if U.attractMode then return end
        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

So when the tests create and destroy those ships, to stop the sound, the tween runs and triggers creation of a new ship. It seems that if I wait long enough after the tests run, that check for attractMode works, but otherwise the tween hits at game time and tosses several ships into the mix. It’s very odd.

Now the real “fix” is probably to let ship spawning move up to Universe, where decisions about game and wave cycles are better made. For now, I think I’ll condition the tween on attractMode as well:

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

That’s probably redundant but I think it’ll fix the problem.

I was mistaken, and finally sorted out why: the FakeUniverse didn’t have attractMode set, so saucers created in the fake universe fired their tweens and so if you started the game right after the tests ran, you got the multiple ship effect. Setting the mode in the fake universe seems to have sorted that.

There’s another thing that I noticed, which is that it is possible for the ship to destroy two objects that crash into it at the same time. I don’t think this should be allowed to happen. The relevant code is this:

function Universe:findCollisions()
    for k, o in pairs(self.objects) do
        for kk, oo in pairs(self.objects) do
            o:collide(oo)
        end
    end
end

Suppose we are processing an object ‘o’ in the outer loop, and suppose it is within collision range of two or more objects. Even though the first one kills it, it’ll still be being compared with all the other objects. So, rarely, it might kill again.

I’m going to protect against that. Should I write a test for it? Yes, but I’m not going to. At least not right now at 5 AM.

function Universe:findCollisions()
    for k, o in pairs(self.objects) do
        for kk, oo in pairs(self.objects) do
            if self.objects[o] then o:collide(oo) end
        end
    end
end

Since we set objects[o] to nil on death, this check should ensure that we don’t process objects after they’ve passed from this mortal plane.

Commit: “autorun codeaunit, no duplicate ships, no duplicate hits”.

Civilized Hour: 0850

Well, I still don’t see a good way to test that loop code. The reason is that the order in which pairs presents objects is literally undefined, so I don’t see a way to ensure there’s something in the outer loop seeing items in the inner loop, two of which can destroy him, before they see him first.

Anyway I’m sure enough of the code. Sometimes you gotta do what you gotta do.

What do we gotta do now?

I was thinking, maybe we should change over to Universe-centric control over respawning and the like. That got me wondering if I could have somehow stopped the tweens that were starting the ship over. That led to rereading the tween documentation with less focus on a specific thing to do, which led me to tween.delay, which does a pure delay with callback, and tween.stop, which, given the id returned from a tween call (who knew?) will stop that tween.

The former will let us simplify some tweens:

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

We’ll try:

function Ship:die()
    local f = function()
        if U.attractMode then return end
        Ship()
    end
    U:playStereo(U.sounds.bangLarge, self, 0.8)
    Explosion(self)
    U:deleteObject(self)
    Instance = nil
    if not U.attractMode then
        tween.delay(6, f)
    end
end

That works nicely indeed. I think we’re safe from the extra ships now but we could imagine saving the tweens somewhere and stopping them. Thing is, for some purposes, like the explosion and splat, autonomous timing is great, but for ship spawning, it may not belong in Ship at all. Let’s find any others we may be using.

The splat uses one to expand its size, so it’s good as it is. Missile just uses the delay, so we’ll fix that:

function Missile:init(pos, step)
    function die()
        self:die()
    end
    self.pos = pos
    self.step = step 
    U:addObject(self)
    tween.delay(3, die)
end

Much less odd and obscure. A small win with these. Saucer has one much like the splat:

function Saucer:init(optionalPos)
    function die()
        if self == Instance then
            self:dieQuietly()
        end
    end
    Instance = self
    U:addObject(self)
    self.shotSpeed = 5
    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
    self.sound = sound(asset.saucerBigHi, 0.8, 1, 0, true)
    tween.delay(7, die)
end

Those are nice. Commit: “use tween.delay”.

I think this is enough for the morning. Let’s quickly sum up.

Summing Up

Well, we’re displaying test results on the screen at startup, and we cleaned up a few little things.

Not much to see here. Next time I’ll try to think of something interesting. Maybe finite number of ships to game over.

Asteroids CodeaUnit