Let’s do collisions. And try to TDD them.

Collisions should be “easy” now, mostly a matter of just implementing the collision loop, running it, and stepping through the issues we encounter. Let’s try to do better, with some real TDD.

We have two tests that might serve as samples:

        _:test("missile hits saucer", function()
            U = FakeUniverse()
            local m = Missile()
            local s = Saucer()
            m:collide(s)
            _:expect(U:destroyedCount()).is(2)
        end)
        
        _:test("saucer hits missile", function()
            U = FakeUniverse()
            local m = Missile()
            local s = Saucer()
            s:collide(m)
            _:expect(U:destroyedCount()).is(2)
        end)

Our fake universe isn’t too clever:

FakeUniverse = class()

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

function FakeUniverse:destroy(anObject)
    self.destroyed[anObject] = anObject
end

function FakeUniverse:destroyedCount()
    return self:count(self.destroyed)
end

function FakeUniverse:count(aTable)
    local c = 0
    for k,v in pairs(aTable) do
        c = c + 1
    end
    return c
end

function FakeUniverse:addObject(missile)
    self.missile = missile
end

function FakeUniverse:addSaucer(saucer)
    self,saucer = saucer
end

Furthermore, it’s following an old convention for adding the saucer, and with that comma there, I don’t see how that could do anything good.

I’ll remove that method and we’ll see what else it needs. It’s not doing much yet. Nor are our collision methods very complete. We’ll try to work it all out.

First let’s consider missile:collide(saucer). We expect the missile to send saucer:collideWithMissile(self). We’d like the missile to check the distances and destroy both. We’ll need to create the objects with locations within kill distance. For now, let’s just put them in the same location: we can test the distances more deeply later.

The missile accepts a position parameter we can use:

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

Saucer does not, so this test will fail on that, but not right away.

function Saucer:collideWithMissile(missile)
    U:destroy(self)
    U:destroy(missile)
end

Right now, the saucer is so afraid of missiles that any missile at all will kill it. Let’s see about checking the distances. Everyone is supposed to respond to killDist() but many objects do not, including Saucer.

This, too, will cause an error.

I’ll begin by putting the distance check right here but soon enough we’ll need to find a better home for it. The way it works is that each object has a value killDist which is essentially its radius. So objects will destroy each other if their distance apart is less than the sum of their kill distances.

I think this should do it:

function Saucer:collideWithMissile(missile)
    local d = self.pos:dist(missile.pos)
    if d < self:killDist() + missile:killDist() then
        U:destroy(self)
        U:destroy(missile)
    end
end

We have an issue with whether we do U:destroy or whether we should send the die message. I think it’s the latter. So …

function Saucer:collideWithMissile(missile)
    local d = self.pos:dist(missile.pos)
    if d < self:killDist() + missile:killDist() then
        self:die()
        missile:die()
    end
end

Let’s make this test work. First error as expected:

17: missile hits saucer -- Saucer:64: attempt to call a nil value (method 'killDist')

Implement that. What should it be? Our existing code treats the missile as having zero radius, that is, its position has to be inside the other object’s boundary to do damage. Let’s stick with that:

function Missile:killDist()
    return 0
end

The big saucer is bigger than the ship. It’s ten pixels across in raw form, and it is drawn at scale 4. That suggests it should be 20, but the ship is 24, what’s up with that?

The ship’s length is 12, not counting the flame, and its scale is 2. That would make me think its kill distance should be 12, not 24. What’s up with that?

The code is definitely treating the value as a radius, so I think I’ll dial ship down to 12 and set saucer to 20.

17: missile hits saucer -- Actual: 0, Expected: 2

Well, they didn’t collide. But Saucer doesn’t accept a starting position. We’ll change that for testing purposes:

function Saucer:init(optionalPos)
    function die()
        self:die()
    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
17: missile hits saucer -- Saucer:57: attempt to call a nil value (method 'deleteObject')

Our fake universe doesn’t support deleteObject. We need to change that:

function FakeUniverse:deleteObject(anObject)
    self.destroyed[anObject] = anObject
end
18: saucer hits missile -- Missile:50: attempt to call a nil value (method 'destroy')

That’s from saucer hits missile. Missile hits saucer is working. Missile has this:

function Missile:collideWithSaucer(saucer)
    U:destroy(self)
    U:destroy(saucer)
end

Naively it should have:

function Missile:collideWithSaucer(saucer)
    local d = self.pos:dist(saucer.pos)
    if d < self:killDist() + saucer:killDist() then
        self:die()
        saucer:die()
    end
end

I say “naively” because we have a lot of duplication happening here. But first make it work, then make it right.

18: saucer hits missile -- Missile:50: attempt to index a nil value (field 'pos')

Hm what’s up with that? Oh. that test hasn’t been given positions yet.

        _:test("saucer hits missile", function()
            local pos = vec2(100,100)
            U = FakeUniverse()
            local m = Missile(pos)
            local s = Saucer(pos)
            s:collide(m)
            _:expect(U:destroyedCount()).is(2)
        end)

Tests run. There’s an interesting side effect when the various timers time out, because they’re affecting the “real” universe, but I’m just accepting that as an interesting aspect of testing in the mirror universe.

So now I could do two more tests to check that they don’t collide if they’re far apart. And then I could do the other 30 combinations of tests. That seems a bit much.

It would drive out the behavior but it might drive me bonkers first.

Or I could write a looping test that does them all at once. That sounds fun. I’ll try that. Maybe just asteroid, missile, and ship to start.

        _:test("deadly collisions", function()
            local pos = vec2(200,200)
            local candidates = { Missile(pos), Asteroid(pos), Ship(pos) }
            for k,c in pairs(candidates) do
                for kk, cc in pairs(candidates) do
                    if cc ~= c then
                        U = FakeUniverse()
                        c:collide(cc)
                        _:expect(U:destroyedCount()).is(2)
                        U = FakeUniverse()
                        cc:collide(c)
                        _:expect(U:destroyedCount()).is(2)
                    end
                end
            end
        end)

I think that’ll do all the pairs with c and cc different, in both directions. Now I just keep fixing the problems:

19: deadly collisions -- Missile:46: attempt to call a nil value (method 'collideWithMissile')

Probably asteroid, don’t you think? I’ll change the test to use ipairs which will do them in order. So now I’m sure its asteroid.

function Asteroid:collideWithMissile(o)
    local dist = self.pos:dist(o.pos)
    if dist < self:killDist() + o:killDist() then
        o:die()
        self:die()
    end
end
19: deadly collisions -- Asteroid:78: attempt to call a nil value (method 'die')

Asteroid die is special:

function Asteroid:die()
    self:bang()
    Splat(self.pos)
    if self.scale ~= 4 then
        Asteroid(self.pos, self.scale//2)
        Asteroid(self.pos, self.scale//2)
    end
    U:deleteObject(self)
end

It’s rather going beyond the TDD requirement to have done that. I copied the split method, which will soon be obsolete. Let’s see what happens now:

19: deadly collisions -- Asteroid:35: attempt to index a nil value (field 'sounds')

That’ll be bang. Let’s just bullet-proof that:

function Asteroid:bang()
    if not U.sounds then return end
    if self.scale == 16 then
        U:playStereo(U.sounds.bangLarge, self)
    elseif self.scale == 8 then
        U:playStereo(U.sounds.bangMedium, self)
    elseif self.scale == 4 then
        U:playStereo(U.sounds.bangSmall, self)
    end
end

That code looks messy to me, we shouldn’t have to send the sound to U, we should send just the name. That’s for another day. And we’re back to this message:

19: deadly collisions -- TestAsteroids:184: attempt to call a nil value (method 'collide')

Someone else doesn’t implement collide. Easily dealt with. However, I’m more concerned about this:

function Asteroid:collideWithMissile(o)
    local dist = self.pos:dist(o.pos)
    if dist < self:killDist() + o:killDist() then
        o:die()
        self:die()
    end
end

function Missile:collideWithSaucer(saucer)
    local d = self.pos:dist(saucer.pos)
    if d < self:killDist() + saucer:killDist() then
        self:die()
        saucer:die()
    end
end

function Saucer:collideWithMissile(missile)
    local d = self.pos:dist(missile.pos)
    if d < self:killDist() + missile:killDist() then
        self:die()
        missile:die()
    end
end

These are, so far, all alike. That is, of course, consistent with our basic rule for mutual destruction: if they’re within the sum of their kill distances, they both die.

We are sworn to remove duplication. We can extract this code as a method. Where should we put it? Right now, our only candidate is to put it on Universe. We have three instances, let’s do it now.

function Universe:mutualDestruction(p,q)
    local dist = p.pos:dist(q.pos)
    if dist < p:killDist() + q:killDist() then
        p:die()
        q:die()
    end
end

We still need all the little methods, including those for other objects that will turn up, like Explosion and Splat. But they’ll now all either be empty, or like this:

function Asteroid:collideWithMissile(o)
    U:mutualDestruction(self,o)
end

It’s time to commit, because I want to think. Commit: “new tests for collision, running red”. I suppose I should ignore that final test, in case I ship this and anyone runs them. Commit: “new collision test ignored”.

Some Questions

We ought to be asking ourselves some questions right about now. There are only two modes for destruction at present, mutual or none. Yet our design calls for a lot of methods in each object.

We will have them all implementing collide, and collideWithXXX for every kind of object there is. Those methods will either do nothing, for the ones that are never destroyed, or they’ll call our new mutualDestruction method. There will be eight or nine kinds of objects, which means 64 or 81 methods. That’s a lot, even though they are small.

We could build some kind of table, but what would be the indices of the table? Our present scheme is sorting out operation by class, and it’s pretty much the standard OO way to do that.

But we could build a table indexed by something else. What if we gave each class a magic number, like 1, 2, 3, 4, and used those numbers to index into a table that told us what to do? That would save us implementing all those methods. It would be more like what Bill Wake was suggesting, and, like so many suggestions one gets, it might be a good one.

I had wanted not to do anything that seemed like a class name check. Giving them numbers is really just another class name.

We’re parsing out kind of a three-way thing:

  1. No object kills objects of the same type. They implement collideWithMyType to return, but do send collideWithMyType in response to collide. This could change for missiles, but could be handled with two kinds of missile.
  2. Some objects just don’t partake of destruction at all. In the current design they just return from collide and from all the collideWithXXX messages that others may send.
  3. Regular folks reply to collide with collideWithMyType, and ignore collideWithMyType and execute mutualDestruction for other incoming types.

It seems like it should come down to less than n-squared methods, but as yet I still don’t see how.

I’m gonna take a break.

OK, Stand Back

I really don’t have a good idea, so I’m going to implement a poor one. Implementing a whole raft of methods all the same is just madness. There are just three cases:

  1. Some of our objects don’t participate in destruction. Splats, explosions, buttons if it came to that. These objects are indestructible.
  2. No destructible object allows its sister objects of the same class to destroy it.
  3. All destructible objects are mutually destroyed when they are in range of another non-sister destructible.

But I don’t see a clean way to implement that. If there is one, other than what I’m about to do now, please clue me in.

So, as Dave Rooney puts it, “Let’s make the mistake of …”.

Subclassing

I propose to implement two superclasses and have all our on-screen objects inherit from one or the other. And we’re going to inherit real behavior from these classes, but not much.

Here’s my plan. There will be two classes, Destructible and Indestructible. Screen objects will inherit from one or the other. The Indestructible class will implement collide and mutualDestruction, both just returning.

Destructible will implement collide, mutualDestruction, and an additional method, unrelated. This last method is called like anObject:unrelated(anotherObject) and returns true unless the two objects are siblings, that is, of the same class.

We’ll talk about the demonic implementation of unrelated shortly.

If my evil plans are correct, once these two classes are implemented (correctly, mind you), and all the on-screen objects inherit properly, collision will be handled entirely in those two classes.

I reckon I’d better TDD this a bit but I think that test I wrote and ignored, plus the other two, should be a decent start.

        _:test("missile hits saucer", function()
            local pos = vec2(100,100)
            U = FakeUniverse()
            local m = Missile(pos)
            local s = Saucer(pos)
            m:collide(s)
            _:expect(U:destroyedCount()).is(2)
        end)
        
        _:test("saucer hits missile", function()
            local pos = vec2(100,100)
            U = FakeUniverse()
            local m = Missile(pos)
            local s = Saucer(pos)
            s:collide(m)
            _:expect(U:destroyedCount()).is(2)
        end)
        
        _:test("deadly collisions", function()
            local pos = vec2(200,200)
            local candidates = { Missile(pos), Asteroid(pos), Ship(pos) }
            for k,c in ipairs(candidates) do
                for kk, cc in ipairs(candidates) do
                    if cc ~= c then
                        U = FakeUniverse()
                        c:collide(cc)
                        _:expect(U:destroyedCount()).is(2)
                        U = FakeUniverse()
                        cc:collide(c)
                        _:expect(U:destroyedCount()).is(2)
                    end
                end
            end
        end)

I’ll begin by removing all my collide and collideWith methods, which will break all those tests but open the door for the new implementation.

As one anticipated, all three tests fail, all because no one understands collide. Now to write Destructible Tell Codea to make a new class, and fill it in like this:

OK, I went off my head, renamed one method and built another:

Destructible = class()

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

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

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

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

This is more code than I’d hope to write for a TDD test, but it is all rather simple. (We’ll see the lie of that when this totally does’t work. But we have hopes.)

I need to move the Destructible tab to the left. Codea is a bit sensitive to order of compilation. Now I have to make asteroid, missile, ship, and saucer inherit from Destructible. I’m starting to wish I’d given it a name that’s easier to type.

This is as simple as changing

Saucer = class()

to this:

Saucer = class(Destructible)

Same for the others, of course. Now let’s run those tests and see what explodes. This will either be gratifying or embarrassing. Both have happened to me before, so I’m good to go.

17: missile hits saucer -- Destructible:21: 
attempt to compare two nil values

That’s this:

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

Well, that’s embarrassing isn’t it. Create two variables and spell them both wrong on the next line.

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

Missile hits saucer and saucer hits missile work correctly. The looping one fails:

19: deadly collisions -- Actual: 0, Expected: 2

It gets four right, and eight wrong. I think we’d better write this out longhand, the loop doesn’t give us enough information.

        _:test("asteroid vs missile", function()
            local pos = vec2(200,200)
            U = FakeUniverse()
            a = Asteroid(pos)
            m = Missile(pos)
            m:collide(a)
            _:expect(U:destroyedCount()).is(2)
        end)

That test runs. I think I see my problem. The objects in the looping test are created before I create FakeUniverses to test them in. So they are pointing off elsewhere when they execute their deleteObject calls. We’ll have to fix that:

Meh. After a number of failed attempts, I’ve not sorted out how to make the looping test work. When I write individual ones like the ones we’ve seen here, they all work. When I try to reuse my FakeUniverse to do the opposite direction, that fails. I’m not sure what I’m missing, but the individual tests are more clear anyway.

Even without writing the others, I’m pretty confident, but once they’re all done I should be just chock full of confidence. I’ll just delete the looping test, it was weird anyway and now I’m not tempted to debug it.

Darn this is tedious. Mindless but tedious and without a good editor on the iPad I don’t see a good way to generate them. I had such high hopes for the loop.

I’ve managed to make two work:

        _: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)

Let me try to do all the ship ones at once. Why not?

Well, one reason why not, and I’m glad I was building up the test slowly, is that the ship ones aren’t working at all. I’m not sure why, and I’m tired. I’m going to quit on a red bar quite soon now. One last look tho.

Found it. Gonna break anyway. See you tomorrow.

Yesterday’s tomorrow today

What was so confusing about some of the objects not working was that they are now all using literally the same implementation. So despite my desire to test all of it, the fact that some of the tests were failing, but some succeeding, was most perplexing.

The problem turned out to be that the ship did not accept any arguments when initialized. It just “knew” where to go. Once I spotted that, the change was easy and we had made one like it elsewhere. Here’s the new code:

function Ship:init(pos)
    self.pos = pos or vec2(WIDTH, HEIGHT)/2
    self.radians = 0
    self.step = vec2(0,0)
    Instance = self
    U:addObject(self)
end

We now have 20 green unit tests, and two ignored. Neither of the ignored ones applies any more, so I’ve removed them

It’s time to commit, I think: “destructible well tested”.

Before we can do our new collision logic, we will need Indestructible logic as well. I’ll write a test for that. The indestructible things include: buttons, which aren’t in the main draw loop yet, splats, also not in there, and Explosions.

If I’m right that there is only one object that’s indestructible, we don’t need, and shouldn’t build the superclass. Instead, we’ll just put the logic directly into Explosion. If and when we get another such class, we’ll worry about the duplication at that time.

This seems like the more conservative thing to do, rather than the more iffy use of inheritance.

        _: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)

And in Explosion:

-- Indestructible

function Explosion:collide(anything)
    -- nope
end

function Explosion:mutuallyDestroy(anything)
    --nope
end

Test runs. Commit: “explosions indestructible”.

I think there’s one thing left to do. Destructible objects will still destroy their siblings, and that’s not OK. All the asteroids would just crash together until they were all gone. Might be fun, but it’s not the game.

Let’s write a test to check that:

        _:test("asteroids don't mutually destruct", function()
            local pos = vec2(333,555)
            U = FakeUniverse()
            a1 = Asteroid(pos)
            a2 = Asteroid(pos)
            a1:collide(a2)
            _:expect(U:destroyedCount()).is(0)
        end)
22: asteroids don't mutually destruct -- Actual: 2, Expected: 0

Failed, as expected. We need to implement unrelated in Destructible and use it.

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

There’s what we want to say … and here’s how we do it:

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

The deep in the bag function getmetatable returns the table of operator definitions that control how everything in Lua works. When you define a class, you’re populating a metatable, and each instance gets the same metatable. So if two objects’ metatables are equal, they are of the same type, and if not, not.

This is really deep in the bag of tricks. Probably at some stage in their career, every programmer will dig into arcane and risky things like this, and with luck, learn to use them rarely and with caution.

Inheritance is pretty deep. Metatables are really down there. But it works a treat.

22: asteroids don't mutually destruct -- OK

Commit: “No destruction of siblings”.

Harvest Time

OK, I can think of no way to put it off any longer. It’s time to harvest the major value of these changes by producing our new, slimmed–down, truly terrific findCollisions:

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

Yes, that’s it. Previously about 40 lines, now 7. This is on top of the many lines we’ve already saved during the course of this exercise.

There’s one little problem, the missiles don’t seem to kill the saucer. Everything else works fine.

Slapping a print into Saucer:die tells me they’re being killed. Somehow they’re coming back to life. I’ve even seen two on the screen a time or two. I think the collision code is good, and the saucer refresh has a bit too much oomph. This will take some exploration, but first I’m committing “new findCollisions”.

After rather more study than I’d like, I noticed that the Saucer was moving itself, rather than calling u:moveObject(self), which meant that its location wasn’t clipped to the screen. Since drawing clips automatically, it showed up just fine but was thousands of pixels away.

I spent more time than I should have figuring out what was going on, including an issue that was allowing more than one saucer at a time. I hammered that out of existence but really don’t see what the defect was.

We’re left with the game running well, except that there is no scoring, because the old “findCollisions` did that, and the new version has no scoring in it.

Scoring will of course need to move to the objects, since findCollisions knows just about nothing about what’s going on. It never knows whether two things collided or not.

I’m going to commit now, “collisions working, no score”.

Score

Score is pretty straightforward, though there are decisions to make. For now, I’ve just done this:

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

Sending score to both participants, with implementations ofscore like this:

function Ship:score()
end

function Missile:score()
end

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

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

Asteroid already had a score function, the others are added. Since we wend it to everyone, objects that don’t give us points just ignore the message.

I’ve tweaked the saucer delay up to 7 seconds between saucer appearances. That will probably want to be randomized or something like that in the future, and there is some kind of a trigger that switches you over to the small saucer, which we haven’t implemented yet.

Finally, now that collisions are ubiquitous and everywhere, the saucer destroys asteroids by shooting them and running into them. You score points for those, and you shouldn’t,

I believe that in the original, the saucer’s bullets do not harm asteroids, and when the saucer does hit an asteroid, the asteroid breaks, the saucer is destroyed, and you don’t get points for either. I’ll check the original source and see if I can suss out whether those interpretations are correct.

For now, we’ll ship this after summing up.

Summing Up

Collisions

The big lesson here is that we have greatly reduced the code involved in dealing with collisions, and generally made it simpler. I say generally, because we did pull inheritance out from fairly deep in the bag, and an even deeper function that tells us whether we’re dealing with an object of the same type as we are.

Neither of these is particularly intricate, and my view is that they were both good decisions, at least when the only other idea I had involved implementing n-squared methods for n - 8 or 9 or so. Basically we just took all those methods and moved them up to the top.

Throughout this series of changes, we did harvest benefit. The code got smaller as we went, though in the interim we also had to slip in some wedges that we knew would come out when we were done. Overall, I feel that most of the changes over the past several articles have been delivering value right away, and certainly we finally cashed in in this article, removing the intricate old collision logic entirely and replacing it with smaller and simpler.

Tests

I did a lot more testing here in this article, and it gave me a lot more confidence that things were working right. In addition it turned up a few problems that mostly came down to missing implementations that weren’t used in the more specialized collisions, and some curious oversights like objects initializing themselves and ignoring my attempt to tell them where to go.

With a scheme for testing in place, I was much more able to tick through until it worked.

However

I did get derailed by the Saucer. I spared you most of the thrashing, but the fact that it’s a singleton, didn’t use the correct convention for moving itself, and can die by timer as well as missile … well, that made me confused for a while.

It comes down, I’d say, to the saucer’s design being slapped in and not cleaned up. There is another issue, which is that of the “standard way” of doing things. It’s not easy to see that an object doesn’t use the standard way, it’s like looking for something that isn’t there: you don’t see it.

The learning there, I guess, is to try to keep things clean, try to do things the same way everywhere, and remove duplication wherever possible. When one idea is implemented in just one place, once we get it right, it’s right for everyone.

Issues

The saucer kills asteroids and gives us free points. And if it crashes into an asteroid, we get the points for the saucer too. We need to fix that, and I predict it’ll be tricky.

Also the saucer runs in a straight line. I thought at one point I had it changing direction, but one way or the other we need to add that.

Summing up summing up

I’m pleased with the code, although not delighted, and pleased to have moved smoothly through a “major” refactoring in small steps with release points in between. It wasn’t perfect, nor should we expect that. It could have been better, and I’m not feeling that we are quite dead center in the best I’ve ever done. But we’re close.

Maybe that’s sad. I think it’s happy.

See you next time!

Zipped Code