I know the voices in my head aren’t real, but they’re sure ticked off at me for the past few days of messing up.

Today our primary mission is not to screw up. Perhaps that should be every day’s mission, but usually things go better. Past few days, the record has been poor:

  • Release a version that was literally unplayable;
  • Hot fix the code and forget to commit the changes;
  • Build all day on the wrong base and scrap everything.

Truth to tell, yesterday was going well, and I’m never reluctant to scrap a few hours’ work and do it over. It generally goes better the next time. And since the hot fix was only two lines and they were documented, it would have been the work of moments to put them in and continue right on.

But the events of the day, and the lateness of the hour (well after lunch) took the wind out of my sails. When you have no wind in your sails, take a break.

Today, again, building the Saucer. And much as I really don’t want to, I’m going to try it again with TDD. Here’s why:

First, I know that I would benefit from more TDD in these games. I know that because I know what it feels like to have tests that justly provide confidence, and in both Asteroids and Invaders, and in most of my Codea work, my tests never quite rise to the level that I’m sure they could.

Second, the logic of the saucer is a bit intricate, because the saucer and the rolling shot never occur at the same time. In the original game, they use the same moving object slot. I suspect that was done to conserve precious memory, but I’m not sure. Be that as it may, emulating that behavior and the rest of its logic seems tricky. TDD is how we deal with tricky.

Third, I’m curious about whether I can drive out a nicer design for the saucer than we have so far.

We’ll see. Let’s get started.

Saucer III

This is at least the third attempt at the saucer. I’m not planning to look at the code for the other two versions, and if I do, I’ll mention it. I’m assuming–certainly without cause–that I’ve absorbed some useful learning in the other attempts, and that it will come to mind.

Fresh Working Copy, no changes pending. Run tests and program. Both work.

Begin with a test. I think today that I’ll start by TDDing the Saucer object and the aspects of its behavior that are amenable to TDDing. That will include starting at the right place, moving in the right direction, stopping at the right place, and maybe at least part of its explosion protocol.

        _:test("Saucer starts right, goes left if shots odd", function()
            local s = Saucer(1)
            _:expect(s:position()).is(vec2(208,185))
            _:expect(s:move()).is(vec2(-2,0))
        end)

This is a bit different from our usual approach in a few ways:

It seems to be assuming that the saucer determines its position and direction based on knowing at creation time whether shots are even or odd. Our other objects are created once and for all. I have no good reason for this, it just seemed direct.

It’s driving out methods, position and move, not looking at internal values. I chose this because I think methods are better for accessing info than reaching inside.

I chose the starting position using some arithmetic on the army setup, and the move vector is based on my reading that the original saucer moved two pixels per step.

The test will fail and drive out some code as follows:

21: Saucer starts right, goes left if shots odd -- Tests:232: attempt to call a nil value (global 'Saucer')
Saucer = class()

function Saucer:init()
end
21: Saucer starts right, goes left if shots odd -- Tests:233: attempt to call a nil value (method 'position')
function Saucer:position()
end

I’m being very conservative, very strict with myself, trying never to write a line of code that isn’t specifically required by the test. Clearly I could type everything in right now, but this is good practice for getting back in the TDD frame of mind.

It’s kind of fun and builds up an enjoyable rhythm when you try doing the absolute minimum to get tests to pass.

21: Saucer starts right, goes left if shots odd  -- Actual: nil, Expected: (208.000000, 185.000000)
21: Saucer starts right, goes left if shots odd -- Tests:234: attempt to call a nil value (method 'move')

We see here an issue with more than one assert per test. While it saves redundant setup, it can be confusing, and now I’m tempted to fix two things rather than one. But I am strong.

function Saucer:position()
    return vec2(208,185)
end

This is our old friend “fake it till you make it”, learned from Kent Beck, Lo! these many years ago. Minimum code to pass, drives out structure, leaves more behavior for subsequent tests.

21: Saucer starts right, goes left if shots odd -- Tests:234: attempt to call a nil value (method 'move')

I’m sure tempted to fix this at least down to returning the literal. But no:

function Saucer:move()
end
21: Saucer starts right, goes left if shots odd  -- Actual: nil, Expected: (-2.000000, 0.000000)
function Saucer:move()
    return vec2(-2,0)
end

I expect green now. And I get it:

21 Passed, 0 Ignored, 0 Failed

Now I remember that it is red-green-refactor. Is there anything to refactor here? I think there is. The name position seems good to me but move is a verb and we’re not making the saucer move, we are asking it how much it plans to move. This is move amount or … maybe step? That’s a verb also. How about stepSize? We’ll go with that.

Saucer = class()

function Saucer:init()
end

function Saucer:position()
    return vec2(208,185)
end

function Saucer:stepSize()
    return vec2(-2,0)
end

And the corresponding change to the test.

This is so good I’m going to commit it. Initial Saucer class and test.

Now we need a new test and it’s pretty clear what it should be:

        _:test("Saucer starts left, goes right if shots even", function()
            local s = Saucer(2)
            _:expect(s:position()).is(vec2(8,185))
            _:expect(s:stepSize()).is(vec2(2,0))
        end)

For some reason this test doesn’t work at all well:

22: Saucer starts left, goes right if shots even  -- Actual: (208.000000, 185.000000), Expected: (8.000000, 185.000000)
22: Saucer starts left, goes right if shots even  -- Actual: (-2.000000, 0.000000), Expected: (2.000000, 0.000000)

This will surely take a few steps. First, we need to accept the shots fired count in init and do something with it. Since I do understand that the issue is odd-even, I can deal with that in init. I start with this:

function Saucer:init(shotsFired)
    self.direction = shotsFired%2 == 0 and 1 or -1
end

Now I have a convenient multiplier for the step size:

function Saucer:init(shotsFired)
    self.direction = shotsFired%2 == 0 and 1 or -1
    self.step = self.direction*vec2(2,0)
end

function Saucer:stepSize()
    return self.step
end

That makes the step size check run in both tests. Now the start. I’m feeling fancy.

function Saucer:init(shotsFired)
    self.direction = shotsFired%2 == 0 and 1 or -1
    self.step = self.direction*vec2(2,0)
    self.pos = vec2(108,185) - self.direction*vec2(100,0)
end

function Saucer:position()
    return self.pos
end

This gives me a green bar. Is it too fancy to be allowed to live? Probably. But I have a green bar, and it’s a good time to break for a chai run. I’ll decide when I get back.

Commit: Two saucer tests run.

Post Chai Run

OK, I accept that that’s too cute to be allowed to live, but let’s deal with a but more capability and then decide what to do. I have other concerns.

First, let’s do something about the end position as well as the start. While I’m at it, let’s rename that position function to startPosition:

function Saucer:init(shotsFired)
    self.direction = shotsFired%2 == 0 and 1 or -1
    self.step = self.direction*vec2(2,0)
    self.startPos = vec2(108,185) - self.direction*vec2(100,0)
end

function Saucer:startPosition()
    return self.startPos
end

        _:test("Saucer starts right, goes left if shots odd", function()
            local s = Saucer(1)
            _:expect(s:startPosition()).is(vec2(208,185))
            _:expect(s:stepSize()).is(vec2(-2,0))
        end)
        
        _:test("Saucer starts left, goes right if shots even", function()
            local s = Saucer(2)
            _:expect(s:startPosition()).is(vec2(8,185))
            _:expect(s:stepSize()).is(vec2(2,0))
        end)

Why? Well, mostly because that’s all I’ve calculated, so call it what it is. Additionally, I have an inkling that when we move to a single object instead of a new one every time, if we do, we’ll like that better. Now the stops. If I were a good person, I’d write two new tests for this. Am I a good person? No, but let’s do it that way anyway just to see how we feel afterward.

        _:test("Saucer stops left if shots odd", function()
            local s = Saucer(1)
            _:expect(s:stopPosition()).is(vec2(8,185))
        end)
        
        _:test("Saucer stops right if shots even", function()
            local s = Saucer(2)
            _:expect(s:stopPosition()).is(vec2(208,185))
        end)

In fact I don’t like this and I’ll tell you why in a moment. For now, let’s make them run.

24: Saucer stops right if shots even -- Tests:250: attempt to call a nil value (method 'stopPosition')

It’s easy to do this part:

function Saucer:stopPosition()
    return self.stopPos
end

But when we look at this, we see that we just can’t follow this pattern and have it be reasonable:

function Saucer:init(shotsFired)
    self.direction = shotsFired%2 == 0 and 1 or -1
    self.step = self.direction*vec2(2,0)
    self.startPos = vec2(108,185) - self.direction*vec2(100,0)
end

The pattern will work like this:

function Saucer:init(shotsFired)
    self.direction = shotsFired%2 == 0 and 1 or -1
    self.step = self.direction*vec2(2,0)
    self.startPos = vec2(108,185) - self.direction*vec2(100,0)
    self.stopPos = vec2(108,185) + self.direction*vec2(100,0)
end

The tests are green, but I can imagine the programmer of tomorrow coming in, looking at that and going “What the–”. I don’t want that, it would scare the cat. Let’s try this for size:

function Saucer:init(shotsFired)
    self.startPos = vec2(  8,185)
    self.stopPos  = vec2(208,185)
    self.step     = vec2(  2,  0)
    if shotsFired%2==1 then
        self.step = -self.step
        self.startPos,self.stopPos = self.stopPos,self.startPos
    end
end

I like that because it initializes all three member variables correctly for one direction, then if the other direction is needed, switches them. One might find the variable swapping a bit weird, but it’s idiomatic Lua so I’ll allow it.

Tests are green. Commit: Saucer stop position correct.

Now I wanted to talk about the tests: They look like this:

        _:test("Saucer starts right, goes left if shots odd", function()
            local s = Saucer(1)
            _:expect(s:startPosition()).is(vec2(208,185))
            _:expect(s:stepSize()).is(vec2(-2,0))
        end)
        
        _:test("Saucer starts left, goes right if shots even", function()
            local s = Saucer(2)
            _:expect(s:startPosition()).is(vec2(8,185))
            _:expect(s:stepSize()).is(vec2(2,0))
        end)
        
        _:test("Saucer stops left if shots odd", function()
            local s = Saucer(1)
            _:expect(s:stopPosition()).is(vec2(8,185))
        end)
        
        _:test("Saucer stops right if shots even", function()
            local s = Saucer(2)
            _:expect(s:stopPosition()).is(vec2(208,185))
        end)

Now first of all, if we’re going to break out stopping, we should break out stepping. But even as they stand now, the tests don’t tell a story. Despite all the handed-down issues with multiple asserts, I’m going to combine them to tell two stories:

        _:test("Saucer starts right, goes left, stops left if shots odd", function()
            local s = Saucer(1)
            _:expect(s:startPosition()).is(vec2(208,185))
            _:expect(s:stepSize()).is(vec2(-2,0))
            _:expect(s:stopPosition()).is(vec2(8,185))
        end)
        
        _:test("Saucer starts left, goes right, stops right if shots even", function()
            local s = Saucer(2)
            _:expect(s:startPosition()).is(vec2(8,185))
            _:expect(s:stepSize()).is(vec2(2,0))
            _:expect(s:stopPosition()).is(vec2(208,185))
        end)

That’s what we want: that package of behavior, all three of those values synchronized.

Commit again. Rearranged saucer tests.

I remain ambivalent about using what amount to getter methods here, instead of just accessing the member variables, but I think it’s better practice and I expect I’ll like it better as time goes on.

One thing is far from clear to me at this point. No, two things. First, is the saucer run at the right coordinate, especially Y, and second, is it going to be moving too fast (I’m sure it is). To answer those concerns, we need to draw a saucer, and move it.

For now, let’s just pop one into the army. It pretty much owns the saucer anyway, both logically and according to our design.

function Army:draw()
    pushMatrix()
    pushStyle()
    for i,invader in ipairs(self.invaders) do
        invader:draw()
    end
    if self.rollingBomb.alive then self.rollingBomb:draw() end
    if self.plungerBomb.alive then self.plungerBomb:draw() end
    if self.squiggleBomb.alive then self.squiggleBomb:draw() end
    self.saucer:draw()
    popStyle()
    popMatrix()
end

function Army:update()
    self:updateBombCycle()
    self:possiblyDropBomb()
    local continue = true
    while(continue) do
        continue = self:nextInvader():update(self.motion, self)
    end
    self.rollingBomb:update(self)
    self.plungerBomb:update(self)
    self.squiggleBomb:update(self)
    self.saucer:update(self)
end

And a creation in Army:init(). This will crash until I draw the saucer and accept update.

function Saucer:draw()
    pushStyle()
    tint(255,0,0)
    sprite(asset.saucer, self.startPos:unpack())
    popStyle()
end

function Saucer:update()
end

This gives me just what I was hoping for:

saucer left

I set the saucer at the start position for two reasons. First, I wanted to see if it looked right. But more to the point, the saucer doesn’t have a current position. It can’t move yet.

Obviously we’d like it to move, and we’d also like it to stop and disappear when it gets to the stopping position.

Our other moving objects have an “alive” flag, so I think we’ll want to use that approach here. Let’s see if we can TDD this in, though.

        _:test("Saucer(even) runs left to right and dies", function()
            local s = Saucer(42)
            _:expect(s:isAlive()).is(true)
        end)

First the flag and method:

function Saucer:isAlive()
    return self.alive
end

(With an init to true of course.)

Test is green, extend it:

        _:test("Saucer(even) runs left to right and dies", function()
            local s = Saucer(42)
            _:expect(s:isAlive()).is(true)
            _:expect(s:position()).is(vec2(8,185))
            s:update()
            _:expect(s:position()).is(vec2(10,185))
        end)

I’m really going wild here, two new asserts at once. Hold my beer.

function Saucer:update()
    self.pos = self.pos + self.step
end

function Saucer:position()
    return self.pos
end

Test is green. Extend:

        _:test("Saucer(even) runs left to right and dies", function()
            local s = Saucer(42)
            _:expect(s:isAlive()).is(true)
            _:expect(s:position()).is(vec2(8,185))
            s:update()
            _:expect(s:position()).is(vec2(10,185))
            for i = 1,120 do
                s:update()
            end
            _:expect(s:position()).is(vec2(208,185))
            _:expect(s:isAlive()).is(false)
        end)

Now this test may be asking more than it should. It expects the saucer never to step past 208, even once. I rather expect that it will be more natural to step it, detect that it’s past 208 and then stop. But let’s accept the situation and see what we get.

function Saucer:update()
    local newPos = self.pos + self.step
    if newPos.x <= self.stopPos.x then
        self.pos = newPos
    else
        self.alive = false
    end
end

I think this might do it, so I’ll run the test. And it’s green. However, for some reason, I rather expected the saucer on the screen to go streaking across, and it didn’t. Oh, that’s in draw, isn’t it:

function Saucer:draw()
    pushStyle()
    tint(255,0,0)
    sprite(asset.saucer, self.startPos:unpack())
    popStyle()
end

Should be:

function Saucer:draw()
    pushStyle()
    tint(255,0,0)
    sprite(asset.saucer, self.pos:unpack())
    popStyle()
end

We didn’t have pos when I tested the drawing position, and I didn’t feel right about putting it in un-driven by a test. Now I expect it to streak across:

across

I think it’s definitely too fast but we’ll deal with that based on better understanding of the old program and watching old videos of it running. For now, we can commit: Saucer moves.

One more little thing and then let’s sum up.

The gunner starts at mid-screen and immediately gets fired upon by the tracking missile. In the real game, the gunner starts at the far left. I want to do that as well. We’ll also probably want to defer targeted missiles a bit but we’ll see about that.

function Player:init(pos)
    self.pos = pos or vec2(104,32)
    self.alive = true
    self.count = 0
    self.missile = Missile()
    self.gunMove = vec2(0,0)
    self.ex1 = readImage(asset.playx1)
    self.ex2 = readImage(asset.playx2)
end

That becomes:

function Player:init(pos)
    self.pos = pos or vec2(8,32)
    self.alive = true
    self.count = 0
    self.missile = Missile()
    self.gunMove = vec2(0,0)
    self.ex1 = readImage(asset.playx1)
    self.ex2 = readImage(asset.playx2)
end

gunner-left

I noticed that the gunner stops on the right at 208 but on the left at zero. It’s 16 wide so at 208 it extends all the way to 224, our max screen width. So I guess zero is OK, but both should perhaps be narrower. We’ll not touch that for now.

Commit: Start gunner on left.

We’re at a good point to stop. It’s 0940, so I’m only maybe 90 minutes of working in, but we have a nice start at the Saucer. We’ll sum up, because I feel like there are some important learnings here.

Summing Up

Before I forget, check out GeePaw Hill’s #4 “Real Programming” video when it hits the streets. It might not be out yet: I’ve seen a preview.

In that video, Hill breaks up his nascent Yatzee game into three layers, View, Model, and Domain. I was a bit surprised that he did it so soon, but he has his reasons. As I interpret them, he’s basically preparing a place to be before needing to be there.

His video inspired me to focus a bit more on the “business logic” side of the saucer, which resulted in a lot more tests, and some pretty decent ones about actual saucer behavior. So a hat tip to Hill for making me think. I hate when that happens.

You could make a case that this was fourth time lucky, but be that as it may, there can be real value i tossing out a quick and dirty implementation and doing it over, especially when it’s only a couple of hours work. If I’d worked for three days and gotten that bastard saucer working, you can bet I wouldn’t tear it out and do it over. But a couple of hours? I know I can do better than that, in less time than it took the first time around.

So that’s a trick to have fairly shallow in the bag: if things don’t feel just right and you’re only a few hours in, consider backing out and starting over. Even more so, if it’s just a few minutes.

Of course, to do this wisely, we need to be very sensitive to things not feeling just right. That’s why I share so many of my thoughts as we go through these articles. First, to give you a sense of what I think about in my dotage, but more important, to help me focus on those little thoughts that we all too often push down and trample over in our zeal to Get Things Done.

For today, though, the big lesson for me is how much more I was able to get under test, in a reasonable fashion, than I’d managed before. The tests for saucer are simple, direct, clear, and non-invasive. And they test not just state but actual behavior.

That’s nice, and better than I’ve managed in the past in these game things.

I’m a happy camper today, even though I still suck at actually playing the game. See you next time!

Invaders.zip