I think today we’ll work on the bomb timing, and targeting the rolling bomb. Often, what I think will happen is what actually happens.

There are two aspects to the inter-bomb timing of the old program that we want to replicate. One is that as the player’s score increases, the rate of firing changes, according to a table of values. The other is that when the invaders spawn (or maybe when the player spawns (or maybe both)) there are two seconds before they fire.

I am imagining that if we loaded two seconds into our inter-shot timer, it would just work, so my plan is to work on the table-driven settings first.

We now have a fixed delay:

function Army:possiblyDropBomb()
    if self:canDropBomb() then 
        self:dropRandomBomb()
    end
end

function Army:canDropBomb()
    return self.bombCycle > self.bombDropCycleLimit
end

It’s bombCycleLimit that’s holding us back, so that’ll be what should looked up from the table. Now to find the old invaders info about what it does.

The reload rate gets faster as the game progresses. The code uses the upper two digits of the player’s score as a lookup into the table at 1AA1 and sets the reload rate to the value in the table at 1CB8. The reload rate for anything below 0x0200 is 0x30. From 0x0200 to 0x1000 the delay drops to 0x10, then 0x0B at from 0x1000 to 0x2000. From 0x2000 to 0x3000 the rate is 8 and then maxes out above 0x3000 at 7. With a little flying-saucer-luck you will reach 0x3000 in 2 or 3 racks.

The table corresponding to this writeup is this:

ShotReloadRate:
; The tables at 1CB8 and 1AA1 control how fast shots are created. The speed is based
; on the upper byte of the player's score. For a score of less than or equal 0200 then 
; the fire speed is 30. For a score less than or equal 1000 the shot speed is 10. Less 
; than or equal 2000 the speed is 0B. Less than or equal 3000 is 08. And anything 
; above 3000 is 07.
;
; 1CB8: 02 10 20 30
;
1AA1: 30 10 0B 08                           
1AA5: 07           ; Fastest shot firing speed

This makes clear that at 1CB8 we find the score value, and that the 1AA1 table is the firing speed. There’s one more detail I wonder about, and that’s whether the score is stored in BCD. I’m pretty sure that it is.

Ah. Here’s the answer:

AlienScores:
; Score table for hitting alien type
1DA0: 10 ; Bottom 2 rows
1DA1: 20 ; Middle row
1DA2: 30 ; Highest row

Since the scores for the invaders are 10, 20, 30 decimal, and these are hex numbers, we know the scores are in BCD. So we can translate the value checks accordingly.

The next question is where to do this resetting. Since we’re counting up and checking for cycle time greater, we can pretty much set the bombCycleLimit whenever we want to. But it seems to me that it makes sense to do it when we add to the score.

Here we run into some “legacy code”, a design that was good for the time but is not so good now:

function Invader:killedBy(missile)
    if not self.alive then return false end
    if self:isHit(missile) then
        self.alive = false
        Score = Score + self.score
        self.exploding =15
        SoundPlayer:play("killed")
        return true
    else
        return false
    end
end

We’re just jamming the value into a global variable Score. This happens to be the only place, other than initialization, where we add to the score. It seems like overkill to create an object for Score, at least not right now, but we can encapsulate the desired behavior this way:

function Invader:killedBy(missile)
    if not self.alive then return false end
    if self:isHit(missile) then
        self.alive = false
        addToScore(self.score) -- <---
        self.exploding =15
        SoundPlayer:play("killed")
        return true
    else
        return false
    end
end
 
-- Main
function addToScore(added)
    Score = Score + added
end

Now we have a convenient central place to do our adjustment. Isn’t that nice? We could even rename the Score variable for extra protection, if we didn’t trust ourselves so much.

What about the adjustment? We could have Main own it, but the drift should in general be away from Main owning ideas. (We might even argue that we should have found a better place than Main for our addToScore function.) We could let it live in GameRunner, on the grounds that it’s a game property. But since the value is used in Army, I think it should be adjusted in Army:

function addToScore(added)
    Score = Score + added
    TheArmy:adjustTiming(Score)
end

function Army:timingValue(score)
    return score<=200  and 0x30 
        or score<=1000 and 0x10 
        or score<=2000 and 0x0B
        or score<=3000 and 0x08
        or 0x07
end

function adjustTiming(score)
    self.bombDropCycleLimit = self:timingValue(score)
end

There’s just one bitty little problem here, which is the scaling of those values. I think we’re only checking and incrementing every third time through, like the original, so I’m hoping this is OK. the values seem pretty short, though, given that we’re using 50 now, for a firing rate of a bit more than once per second. We’ll see.

A separate issue is testing. Not TDD style: I decided this wasn’t worth that (I may be wrong). But game-play testing. It’ll be a cold day when I get scores over 3000.

I think I’ll add a parameter to the game, that will add n thousand to the score:

function setup()
    parameter.integer("ScoreHelp",0,4)
    ...

I’d like that value to show up in the score display, for better understanding of how I’m doing. What’s a decent way to do that? I think I’ll just add in the value at display time, and in the timing adjustment. This is a temporary thing anyway.


    text("SCORE " .. tostring(Score+ScoreHelp*1000), 144, 4)


function Army:timingValue(rawScore)
    local score = rawScore + ScoreHelp*1000
    return score<=200  and 0x30 
        or score<=1000 and 0x10 
        or score<=2000 and 0x0B
        or score<=3000 and 0x08
        or 0x07
end

Let’s see what happens. Nothing happens, because the game doesn’t know that I’ve scored unless I shoot something to trigger the cycle update. I guess I have to do at least some work here.

Then what happens is:

Main:137: attempt to call a nil value (method 'adjustTiming')
stack traceback:
	Main:137: in function 'addToScore'
	Invader:45: in method 'killedBy'
	Army:172: in method 'checkForKill'
	Army:166: in method 'processMissile'
	Missile:26: in method 'draw'
	Gunner:42: in function 'drawGunner'
	GameRunner:22: in method 'draw'
	Main:26: in function 'draw'

I left the Army off this:

function Army:adjustTiming(score)
    self.bombDropCycleLimit = self:timingValue(score)
end

A TDD test would have caught this sooner, even a trivial one. Make a note of that.

Here’s the rain of fire at score 4000:

score 4000

I haven’t found any on-line videos of scores above about 1000, so that wasn’t much help. It may be that after game-play we’d want to scale that count number a bit. I certainly notice the speedup between no score and 200. It steps from 0x30 to 0x10, or from 48 down to 16, three times faster. And the 0x30 is close to my original rate of 50, so I think we’ve probably got the right values. That means that the game is a monster to play, at least for me, and with the controls we have on the iPad.

I think we’re done here. I’ll remove the parameter and its references and commit: score-dependent bomb rate.

Targeting

OK, now what about targeting? The rolling bomb is supposed to be targeted. Based on my reading of the code (and mostly the comments) what this means is that when the rolling bomb wants to fire, it check’s the player’s x coordinate, and if there is a non-empty column of invaders over that position, it drops a bomb from that column. If there is no such column, it doesn’t fire.

Our current code looks like this:

function Army:dropRandomBomb()
    local bombs = {self.rollingBomb, self.plungerBomb, self.squiggleBomb}
    local bombType = math.random(3)
    self:dropBomb(bombs[bombType])
end

function Army:dropBomb(aBomb)
    if aBomb.alive then return end
    self.bombCycle = 0
    aBomb.alive = true
    local col = aBomb:nextColumn()
    local leader = self.invaders[1]
    local leaderPos = leader.pos
    local leaderX = leaderPos.x
    local bombX = 8 + leaderX + (col-1)*16
    aBomb.pos = vec2(bombX, leaderPos.y - 16)
end

Our bombs aren’t objects, and they don’t know their type. If they were objects, we could perhaps override nextColumn. We’d have to deal with the possibility that it wouldn’t find one but that would be easy enough.

What is nextColumn like?

function Bomb:nextColumn()
    local next = self.columnTable[self.columnIndex]
    self.columnIndex = self.columnIndex+1
    if self.columnIndex > self.columnEnd then
        self.columnIndex = self.columnStart
    end
    return next
end

Let’s do an override of this the old fashioned way, with a flag, targeted, false except in the rolling shot. So, in init:

function Bomb:init(pos, shapes, explosion, columnStart, columnEnd)
    self.columnStart = columnStart
    self.columnEnd = columnEnd
    self.columnIndex = columnStart
    self.pos = pos
    self.shapes = shapes
    self.explosion = explosion
    self.shape = 0
    self.cycle = 2
    self.alive = false
    self.explodeCount = 0
    self.columnTable = {0x01, 0x07, 0x01, 0x01, 0x01, 0x04, 0x0B, 0x01, 0x06, 0x03, 0x01, 0x01, 0x0B, 0x09, 0x02, 0x08, 0x02, 0x0B, 0x04, 0x07, 0x0A}
end

We can either pass another flag, or do something weird with columnStart or columnEnd. The latter would be vaguely obscene. We’ll have a flag:

function Bomb:init(pos, shapes, explosion, targeted, columnStart, columnEnd)
    self.targeted = targeted
    self.columnStart = columnStart
    self.columnEnd = columnEnd
    ...


function Army:defineBombs()
    ...
    self.rollingBomb = Bomb(vec2(0,0), bombTypes[1], bombExplosion, true, 1,16)
    self.plungerBomb = Bomb(vec2(0,0), bombTypes[2], bombExplosion, false, 1,16)
    self.squiggleBomb = Bomb(vec2(0,0), bombTypes[3], bombExplosion, false, 7,21)
end

Now, we can do something about the column selection.

I think we’d better TDD this, it’s rather intricate.

A bit of up-front design here. The column covering the gunner is a function of the current position of the lead invader. The invader columns are 16 wide. The gunner’s coordinate can be anything between 0 and 208, and it has a width of 16.

Let’s write a test. An easy one, which will be hard enough:

        _:test("rolling shot finds correct column", function()
            local theArmy = Army()
            setupGunner()
            local bomb = theArmy.rollingBomb
            theArmy:setPosition(10,180)
            Gunner.pos = vec2(10,32)
            local col = bomb:nextColumn()
            _:expect(col).is(1)
        end)

We set up army, gunner, and a bomb. We invented a new command on Army, setPosition, which is supposed to line up the whole army on that position. We don’t really have anything like that now: we set up the invaders once and then increment them about. I don’t think we’ll need to fiddle the invaders but we’ll see. Anyway we align the Gunner with the Army, call nextColumn on the bomb and expect the first column number 1 because 1 origins.

This will drive out some behavior. First, setPosition:

function Army:setPosition(pos)
    self.pos = pos
end

This isn’t updated by anyone, but we’ll deal with that in due time. Curiously the test passes now:

19: rolling shot finds correct column -- OK

I guess it accidentally returns 1 from the table. No problem:

        _:test("rolling shot finds correct column", function()
            local theArmy = Army()
            setupGunner()
            local bomb = theArmy.rollingBomb
            theArmy:setPosition(10,180)
            Gunner.pos = vec2(10,32)
            local col = bomb:nextColumn()
            _:expect(col).is(1)
            Gunner.pos = vec2(26,32)
            col = bomb:nextColumn()
            _:expect(col).is(2)
        end)

That fails nicely:

19: rolling shot finds correct column -- Actual: 7, Expected: 2

Now we get to do some work.

function Bomb:nextColumn()
    if self.targeted then return self:targetedColumn() end
    local next = self.columnTable[self.columnIndex]
    self.columnIndex = self.columnIndex+1
    if self.columnIndex > self.columnEnd then
        self.columnIndex = self.columnStart
    end
    return next
end

As I start the real code, I realize I only care about the x position. We’ll go back and deal with that when next this works.

OK, I kind of pulled this out of um, the air:

function Bomb:targetedColumn()
    local gunnerPos = Gunner.pos.x
    local armyPos = TheArmy:xPosition()
    local relativePos = gunnerPos - armyPos
    local result = 1 + relativePos//16
    return math.max(math.min(result,11),1)
end
function Army:setPosition(pos)
    self.invaders[1].pos = pos
end

function Army:xPosition()
    return self.invaders[1].pos.x
end

I changed the Army’s setPosition thing to store the position in the first alien, and to fetch xPosition from there. The code does not seem to work. By which I mean, it doesn’t work:

~~~lua 19: rolling shot finds correct column – Actual: 1.0, Expected: 2


Let's verify the test:

~~~lua
        _:test("rolling shot finds correct column", function()
            local theArmy = Army()
            setupGunner()
            local bomb = theArmy.rollingBomb
            theArmy:setPosition(10,180)
            Gunner.pos = vec2(10,32)
            local col = bomb:nextColumn()
            _:expect(col).is(1)
            Gunner.pos = vec2(26,32)
            col = bomb:nextColumn()
            _:expect(col).is(2)
        end)

A lot of yak shaving has told me, so far, that jamming that value into the first invader doesn’t work. I push 10 into it, and the call to xPosition is returning 16. The first invader’s position at that point is (16,105), which is his official initialized value. How is it that it’s getting set back?

I’ll add an expect:

        _:test("rolling shot finds correct column", function()
            local theArmy = Army()
            setupGunner()
            local bomb = theArmy.rollingBomb
            theArmy:setPosition(vec2(10,180))
            _:expect(theArmy.invaders[1].pos).is(vec2(10,180))
            Gunner.pos = vec2(10,32)
            _:expect(theArmy.invaders[1].pos).is(vec2(10,180))
            local col = bomb:nextColumn()
            _:expect(theArmy.invaders[1].pos).is(vec2(10,180))
            _:expect(col).is(1)
            Gunner.pos = vec2(26,32)
            col = bomb:nextColumn()
            _:expect(col).is(2)
        end)

Ah. The problem is this code:

function Bomb:targetedColumn()
    local gunnerPos = Gunner.pos.x
    local armyPos = TheArmy:xPosition()
    local relativePos = gunnerPos - armyPos
    local result = 1 + relativePos//16
    print("relpos", gunnerPos, "-", armyPos,"+",relativePos, "//16=", relativePos//16, "+1=", result)
    return math.max(math.min(result,11),1)
end

This refers to the global TheArmy, which is the official one, not the one in our test. Right now, I think I’ll patch this to work, but the better solution is to notice that this bomb method wants either to be a method on Army (most likely) or on Gunner. It refers to no bomb values at all.

First I want to make it work. I’m very tempted to make it right but on a red bar, suffering from recent confusion, this isn’t a super idea. So …

        _:test("rolling shot finds correct column", function()
            local cacheArmy = TheArmy
            TheArmy = Army()
            setupGunner()
            local bomb = TheArmy.rollingBomb
            TheArmy:setPosition(vec2(10,180))
            _:expect(TheArmy.invaders[1].pos).is(vec2(10,180))
            Gunner.pos = vec2(10,32)
            _:expect(TheArmy.invaders[1].pos).is(vec2(10,180))
            local col = bomb:nextColumn()
            _:expect(TheArmy.invaders[1].pos).is(vec2(10,180))
            _:expect(col).is(1)
            Gunner.pos = vec2(26,32)
            col = bomb:nextColumn()
            _:expect(col).is(2)
            TheArmy = cacheArmy
        end)

This passes. I can remove all those extra checks, and set up a few more cases.

        _:test("rolling shot finds correct column", function()
            local cacheArmy = TheArmy
            TheArmy = Army()
            setupGunner()
            local bomb = TheArmy.rollingBomb
            TheArmy:setPosition(vec2(10,180))
            Gunner.pos = vec2(10,32)
            local col = bomb:nextColumn()
            _:expect(col).is(1)
            Gunner.pos = vec2(26,32)
            col = bomb:nextColumn()
            _:expect(col).is(2)
            Gunner.pos = vec2(30,32)
            _:expect(col).is(2)
            Gunner.pos = vec2(0,32)
            _:expect(col).is(1)
            TheArmy = cacheArmy
        end)

This fails:

19: rolling shot finds correct column -- Actual: 2.0, Expected: 1

That’s the last test, with the (0,32). What’s up with that?

Might help if I’d make the call.

        _:test("rolling shot finds correct column", function()
            local cacheArmy = TheArmy
            TheArmy = Army()
            setupGunner()
            local bomb = TheArmy.rollingBomb
            TheArmy:setPosition(vec2(10,180))
            Gunner.pos = vec2(10,32)
            local col = bomb:nextColumn()
            _:expect(col).is(1)
            Gunner.pos = vec2(26,32)
            col = bomb:nextColumn()
            _:expect(col).is(2)
            Gunner.pos = vec2(30,32)
            col = bomb:nextColumn()
            _:expect(col).is(2)
            Gunner.pos = vec2(0,32)
            col = bomb:nextColumn()
            _:expect(col).is(1)
            TheArmy = cacheArmy
        end)

This runs. One more check, which will be well off the other end:

            Gunner.pos = vec2(208,32)
            col = bomb:nextColumn()
            _:expect(col).is(11)

The nextColumn function is working as specified, return the column over the gunner, or the closest one to it. (That’s not what I thought it was, I thought it wouldn’t fire if it couldn’t target, but further reading tells me that it just does its best. There is something about skipping a turn, but I’ve not figured that out.

At this point, I expect to see the rolling bombs falling over the gunner all the time. Let’s run and see.

I need some help making sure that all rollers are over the gunner. I’m going to color them red.

This code is definitely working. Here’s a video.

targeted

Amusingly, while filming that, I let myself get killed by a non-targeted bomb. I’m a programmer, Jim, not a gamer.

I’ll remove the red and commit: targeted rollers.

It’s well past my early lunch time. I’ll go grab that and sum up afterward.

Remind me to talk about the learning from the testing.

Summing Up

We actually got done what we had in mind, so that’s a good thing. I think we ran a bit over on time, but I never felt truly buried or in trouble, though that global reference had me guessing for longer than it should. Also it should have never happened: the method doesn’t really belong there, and whoever does it, they should be passed anyone they want to converse with as parameters.

Which reminds me, I was so ready for lunch that I didn’t refactor that. And thinking of it now, I wonder whether we might do better to have the whole calculation done by the army and the firing directed “from above” as it were. We’ll look into that tomorrow.

I’m glad I chose to TDD the feature. It helped me focus on what I needed to do, and made it much easier to work out why it wasn’t working at first. Had I not had those tests, it would have been quite confusing, because I’d never really know any of the gozintas and gozoutas as it went along on the screen.

The test should have given me more of a clue that the design was weird, when I had to push my local army into the global one. That’s of course one of the things that happens with globals and testing. You often have to mock them, fake them, hammer them, and work around them. Of course I’ve been living with globals in the absence of ways I like better. Even had the army been set up as a singleton, we’d have had to hammer that.

Nonetheless, it was a problem until I figured out that hammering was necessary, it was probably a mistake not to find a way to avoid the use of the global right away, and as things stand now it’s a clear indication of a design issue.

But we’re not into Rework Avoidance Theory here, we accept and embrace that we’re evolving the program, and we’ll evolve that to better, probably in the next installment.

I already know what I’ll call it: “Space Invaders 42”! Neat, huh?

See you then!

Invaders.zip