We’ll continue our code review, with particular attention to our tweens, especially that tricky one.

Don’t imagine that I’m beating my breast over the problem with the saucer sound. History tells me that doing it that way was probably identifiable as a mistake, because that tween calling back and setting up another tween is really quite deep in the bag of tricks.

The only way we learn not to dig too deeply into the bag of tricks is to do it once in a while and discover that it wasn’t as great an idea as it might be.

I’ve been learning some lessons all my life, and if I found learning to be painful I couldn’t keep learning. Nor should you, in the unlikely event that you ever make a mistake.

I have, however, been beating my brain, because so far, I’ve not been able to figure out how it was happening. The code looks air-tight to me and all the traps I set in it to find things going wrong never trapped anything. I am tempted to pull the code that was failing and dig further, and one day I just might. Meanwhile, of course, we have good old-fashioned straightforward code in there now, with almost no way for the sound to double up.

Different Approaches

I was thinking this morning in the shower, as one does, that while programming this old game in Codea feels very much like the old days – it feels “close to the metal”, in a way that most programming today doesn’t – it’s different than it was before in some important ways.

I think the most important difference is in the creation and destruction of objects. We think nothing, today, of creating a missile, letting it run, letting it die and be forgotten, while creating another and another. When an asteroid splits, we destroy it and create two new ones. We don’t even rescale and reuse the old one as one of the pair.

In the olden days, we’d never do that. The old program has fixed slots for a small number of objects. It doesn’t create a ship, let it get destroyed, and create another. It has one ship and it has special state to show the ship when it’s alive, hide it when it’s dead or in hyperspace, then show it again when the time is right.

In the olden days we certainly used subroutines and functions, but not as freely as we do today. Reading the old program, most of the functions are there to break out and identify a particular computation or to draw a particular component. That’s much as we do today, identifying some program concept and writing a patch of code to carry it out.

But today I feel no qualms about grabbing a few lines of code and refactoring them out of one place into another, and that’s without any refactoring support in Codea. With one of today’s smart IDEs, we can split up and combine code with machine-level accuracy.

It’s quite different and mostly works to our advantage, though of course it requires skill to know what to do, even if the machine helps us do it.

We can divide up and parse our mistakes along many dimensions, and each such parsing should suggest things to do about them, and “try harder” isn’t a very good answer.

Dropping a ball

Many of my mistakes are things I’ve missed, places where I’ve dropped one of the three or five balls that have to be in the air to make something happen. When this happens, often the best answer is “make it require fewer balls in the air”. But we simply love our ability to handle those complex juggling bits of our act. And sometimes we really need that ability, because there’s no other way, or we can think of no other way.

However. Lots of balls in the air is not as good as fewer balls in the air. That’s one way today’s Asteroids program is inherently better than the old one, in my opinion:

Each important aspect of the game is broken out into a separate, easy to identify, easy to consider, nicely separate chunk.

In the olden days, we didn’t have the space or the tools to do that so readily.

Dot (.) or colon (:)

One of my most common errors, however, is two drop a little tiny ball, one of the dots that make up a colon. Very often, when I mean to send a message to an object:

anObject:doSomething()

I wind up writing

anObject.doSomething()

Codea is perfectly happy to let me write that, because it means something in Lua, even though it doesn’t mean what I had in mind. So that code compiles, almost always.

Codea will even let me say anObajkt.doSomething(), because it is just happy as a clam to assume there will be a value in a global of that name that it has never seen before, when the time comes to execute that code.

My only hopes regarding that common mistake are that I’ll notice before compiling, which I sometimes do, or that I’ll test enough to run into that code and see the error. Sometimes that happens as well.

If I’ve been writing lots of tests around that code, odds are a test will fail and I’ll see the failure and fix the problem. If my tests in that area are weak or missing, I still have a chance to find it when I play-test the game.

Usually that works. Too many times, it doesn’t.

I try to write more tests when something slips through. I’m tempted not to mention it, since you’d probably never know I’d made the mistake if I didn’t tell you. But I want you to know that I make mistakes, often very dumb ones, and that I struggle to find effective ways to make fewer mistakes or to detect them so soon that they don’t seem so much like mistakes.

I want you to know that, because I suspect that you may make mistakes too (any day now, it could happen) and when you do, I hope you can shake it off and bend some attention to finding effective ways to make fewer and find them sooner.

OK. Now then let’s go look at what is quite possibly a mistake:

Ships … in … Spaaaace …

The hyperspace code partakes of that deep in the bag trick of a tween calling a tween, the trick that resulted in duplicate sounds in the saucer:

function Ship:enterHyperspace()
    local appear = function()
        if self:safeToAppear() then
            U:addObject(self)
            self.scale = 10
            tween(1, self, {scale=2})
        else
            self:signalUnsafe()
            tween.delay(3, self.hyperReturn)
        end
    end
    U:deleteObject(self)
    self.pos = self:randomPointIn(100,200, WIDTH-100, HEIGHT-100)
    self.hyperReturn = appear
    tween.delay(3,appear)
end

Let’s break that into two bits visually:

function Ship:enterHyperspace()
    U:deleteObject(self)
    self.pos = self:randomPointIn(100,200, WIDTH-100, HEIGHT-100)
    self.hyperReturn = appear
    tween.delay(3,appear)
end

This outer bit is pretty simple and if we had said this, it’d be even more reasonable:

function Ship:enterHyperspace()
    U:deleteObject(self)
    self.pos = self:randomPointIn(100,200, WIDTH-100, HEIGHT-100)
    tween.delay(3,appear)
end

This code just removes the ship from the universe’s attention, picks a location to appear, and delays three seconds after which it calls appear.

Now if appear were this simple:

    local appear = function()
            U:addObject(self)
    end

We’d have no concerns at all: Put that together:

function Ship:enterHyperspace()
    local appear = function()
            U:addObject(self)
    end
    U:deleteObject(self)
    self.pos = self:randomPointIn(100,200, WIDTH-100, HEIGHT-100)
    tween.delay(3,appear)
end

I think that would be just fine. Even if we added in the little tween trick to make the ship drop in, it would still be fine:

function Ship:enterHyperspace()
    local appear = function()
            U:addObject(self)
            self.scale = 10
            tween(1, self, {scale=2})
    end
    U:deleteObject(self)
    self.pos = self:randomPointIn(100,200, WIDTH-100, HEIGHT-100)
    tween.delay(3,appear)
end

Nothing tricky there at all. But this is the real appear function:

    local appear = function()
        if self:safeToAppear() then
            U:addObject(self)
            self.scale = 10
            tween(1, self, {scale=2})
        else
            self:signalUnsafe()
            tween.delay(3, self.hyperReturn)
        end
    end

The tricky bit is in the else. We signal unsafe (which does nothing but used to beep) and then we “extend” the hyperspace delay. But we can’t restart a delay, it turns out (we tried) so we fire a new delay tween. That can’t refer to the closure it’s inside, so we saved the closure outside and refer to it inside.

If you’re following that, good for you. If you’re not following, you’re proving my point. This code does too many tricky things. It calls a closure, which is pretty much standard Codea, but it also tucks that closure away and reuses it. It should work. It does work. But isn’t it way too clever?

Isn’t this a darling we should kill?

Truth be told, I don’t know why the saucer example fails, though I think it has to do with timing problems among the various ways the saucer can die. The ship is alive throughout this process, it’s just hiding from the universe. So I really think this one cannot fail.

But can it be simpler? Can it take better advantage of the fact that the ship is alive, just hiding? Describing the situation makes me want to try.

Here goes. My plan, tentatively, is to do a simple hide/appear tween, and have that appear call a normal ship method, tryToAppear, which will see if it is safe to appear and if it is, appear, and if not, set a separate tween to call a duplicate appear inside that function.

That wasn’t clear even to me, but I think I know what I mean well enough to do it.

Let’s try:

function Ship:enterHyperspace()
    local tryToAppear = function()
        self:tryToAppear()
    end
    U:deleteObject(self)
    self.pos = self:randomPointIn(100,200, WIDTH-100, HEIGHT-100)
    tween.delay(3,tryToAppear)
end

function Ship:tryToAppear()
    local tryAgain = function()
        self:tryToAppear()
    end
    if self:safeToAppear() then
        U:addObject(self)
        self:dropIn()
    else
        self:signalUnsafe()
        tween.delay(3,tryAgain)
    end
end

function Ship:dropIn()
    self.scale = 10
    tween(1, self, {scale=2})
end

I tested this extensively with the aid of a random number in safeToAppear, and it seems to work just fine.

Do you find this implementation any easier to understand and to believe? I do. Enough to keep it? I do. I think it would be just as complicated with an explicit time check and with the ship outside the universe, who would do the checking?

This implementation is basically the same, with the main difference being that we don’t use the trick of saving the closure and reusing it. Instead, we get a new one every time and just use it once.

I’m going to keep this as a legitimate use of tween.delay, and I’m betting it won’t bite me. If it does, you can be sure I’ll face up to the facts and write about them.

Summing Up

That’s enough for today, it’s 1445 and time for some recreation. I got a very late start today, because I had to finish yesterday’s article today.

This ship hyperspace code is still a bit tricky. You’d think it could be simpler, like

    vanish()
    waitAwhile()
    while(notSafeToComeBack())
        waitAwhileMore()
    end
    comeBack()

Since we can’t really hang waiting for time to expire, at least not without inventing threads or something, we really can’t write anything that simple. What we have is legitimate tween usage, I think, and I believe it’s bullet-proof.

And it’s still pretty darling, and we’d be fools to trust it.

Commit: convert hyperspace return to be less quaint.

Next time we’ll look at something else. We need to consider more mistakes, and infelicities.

See you then!

Asteroids.zip