Users observe the need for more racks of invaders. That’s going to get … interesting. Also Game Over if they reach bottom..

When you shoot down all the invaders, you’re supposed to get another “rack” of them. I’m told that: I’ve never managed to get them all. The new racks start at lower and lower positions on the screen. There’s a table of values in the assembly code for that.

An additional feature is that when the invaders get to the bottom row, it’s game over. Right now, I think they just keep going. I’m tempted to do this one first, because it’s easy.

The good news is that there’s a place where we know that the aliens are all gone. Or at least we could know it: the facts are there:

function Army:handleCycleEnd()
    if self.invaderNumber > #self.invaders then
        self.armySize = self.invaderCount
        self.invaderCount = 0
        self.invaderNumber = 1
        self:adjustMotion()
    end
end

On each invader update, we check to see if we’ve done all of them. We update armySize, which is used in Sound to change the cycle time of the Ominous Tones. The value we use, invaderCount is a dynamic count of the number of invaders who report for duty during the update cycle, that is, the live ones. So when that value is zero, all the invaders have gone to their reward. This is the point to trigger a new rack.

The question is how to do it. I believe the answer is to call the MarshallingCenter for a new army:

function Army:marshalTroops(runner)
    self.invaders = MarshallingCenter(runner):marshalArmy(self)
end

I noticed those references to runner. No one passes a parameter to the creation of the MarshallingCenter, and it ignores the parameter anyway:

function MarshallingCenter:init()
    local vader11 = readImage(asset.inv11)
    local vader12 = readImage(asset.inv12)
    local vader21 = readImage(asset.inv21)
    local vader22 = readImage(asset.inv22)
    local vader31 = readImage(asset.inv31)
    local vader32 = readImage(asset.inv32)
    self.vaders = {
    {vader31,vader32},
    {vader21,vader22}, {vader21,vader22},
    {vader11,vader12}, {vader11,vader12}
    }
    self.scores = {30,20,20,10,10}
end

So we can remove those runner things right now and commit: removed unused parameter creating MarshallingCenter.

I’m a bit irritated by knowing that since we don’t save the MarshallingCenter, we will reread the assets in the init, but in principle, this should start a new rack when all the invaders are gone:

function Army:handleCycleEnd()
    if self.invaderNumber > #self.invaders then
        if self.invaderCount == 0 then
            self:marshalTroops()
        end
        self.armySize = self.invaderCount
        self.invaderCount = 0
        self.invaderNumber = 1
        self:adjustMotion()
    end
end

Now we know I’ll never get down to zero invaders, even with the cheat switch set, so I’ll test it at an easier number … oh … 54.

function Army:handleCycleEnd()
    if self.invaderNumber > #self.invaders then
        if self.invaderCount <= 54 then
            self:marshalTroops()
        end
        self.armySize = self.invaderCount
        self.invaderCount = 0
        self.invaderNumber = 1
        self:adjustMotion()
    end
end

This works well. As soon as I shoot an invader, a new complete rack appears, up where they always start. However, they start dropping bombs right away. I’d like to have that two-second delay before bombing occurs.

There is also a bit of a nicety in the original program, which is that the ranks of invaders appear sequentially, bottom to top. This is an artifact of the drawing algorithm used, which only draws one alien at a time, relying on screen persistence, I guess, to make it look like everyone’s always there. I’m not sure I want to go to the trouble to replicate that, but we might want some kind of indication that a new rack has appeared. Of course, if you’ve just killed the last invader in one rack, a new one appearing will be pretty obvious.

First, though, since this works, let’s set that check back to zero and commit: new rack of invaders after all gone.

Now let’s see about that firing delay. That happens now when a new gunner spawns. We’ll find it somewhere in Army though. Ah:

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

So we’re looking for the handling of the weaponsAreFree flag. Not free as in beer, by the way. It’s set here:

function Army:weaponsFree()
    self.weaponsAreFree = true
end

function Army:weaponsHold()
    self.weaponsAreFree = false
end

And that’s called here:

function GameRunner:spawn(isZombie)
    Player:instance():spawn(isZombie)
    self:setWeaponsDelay()
end

function GameRunner:playerExploding()
    TheArmy:weaponsHold()
end

function GameRunner:setWeaponsDelay()
    self.weaponsTime = ElapsedTime + 2
end

I showed some surrounding code there. This isn’t as clear as we’d like, is it? Technical debt: the residuum of incomplete ideas, still residing in the code. I presume that we’ll be checking the weaponsTime in our update:

function GameRunner:update60ths()
    self.time120 = self.time120 + (DeltaTime < 0.016 and 1 or 2)
    if self.time120 < 2 then return end
    self.time120 = 0
    Player:instance():update()
    TheArmy:update()
    if self.weaponsTime > 0 and ElapsedTime >= self.weaponsTime then
        TheArmy:weaponsFree()
        self.weaponsTime = 0
    end
end

So this is a 2 second timer for weapons being off but I don’t like that the weaponsHold and weaponsTime are being set separately. You’d think they were linked.

I think what’s going on here is that the Player sends playerExploding to GameRunner, and we happen to know that there may be a spawn call later. (Since we spawn zombies after GameOver, we know there will be a spawn call.)

The spawn call sets the weaponsDelay:

function GameRunner:spawn(isZombie)
    Player:instance():spawn(isZombie)
    self:setWeaponsDelay()
end

This seems too intricate for me. It works because certain things happen in a certain order. This is called “Temporal Coupling”, one of a handful of forms of coupling in code. Coupling is necessary, and temporal coupling keeps everything from happening all at once, so to a degree we need it. On the other hand, too much coupling generally makes code hard to understand and therefore hard to change. Temporal coupling is particularly tricky in that regard, because it’s hard to see. We can see that all these functions do these things, but unless we sort of simulate the program in our head, we’re not sure which does what when.

Let’s do ourselves a bit of a favor here, and put a new method on GameRunner:

function GameRunner:twoSecondWeaponsHold()
    TheArmy:weaponsHold()
    self:setWeaponsDelay(2)
end

The setWeaponsDelay unconditionally sets 2. Let’s give it the parameter suggested above, and default it to 2.

function GameRunner:setWeaponsDelay(seconds)
    self.weaponsTime = ElapsedTime + seconds or 2
end

Now where we start the new rack:

function Army:handleCycleEnd()
    if self.invaderNumber > #self.invaders then
        if self.invaderCount <= 0 then
            self:marshalTroops()
            Runner:twoSecondWeaponsHold()
        end
        self.armySize = self.invaderCount
        self.invaderCount = 0
        self.invaderNumber = 1
        self:adjustMotion()
    end
end

I expect this to give me a little time. I’ll put my 54 back in and try again. Maybe I’ll do 50.

But first I had to put in these parentheses:

function GameRunner:setWeaponsDelay(seconds)
    self.weaponsTime = ElapsedTime + (seconds or 2)
end

The bomb delay does work. I’m going to double check by setting it longer. Yes, it’s really working. I think, though, that we should request a weapons hold, and we should specify the time we want. So:

function GameRunner:requestWeaponsHold(seconds)
    TheArmy:weaponsHold()
    self:setWeaponsDelay(seconds or 2)
end

function Army:handleCycleEnd()
    if self.invaderNumber > #self.invaders then
        if self.invaderCount <= 50 then
            self:marshalTroops()
            Runner:requestWeaponsHold(2)
        end
        self.armySize = self.invaderCount
        self.invaderCount = 0
        self.invaderNumber = 1
        self:adjustMotion()
    end
end

OK, set my count back to zero and commit: new rack has weapons hold.

The feature works. Its implementation isn’t bad, but I’m concerned about the fact that the timing isn’t obvious in most cases.

I think this should be more explicit:

function GameRunner:spawn(isZombie)
    Player:instance():spawn(isZombie)
    self:setWeaponsDelay()
end

It’s assuming there is a weapons hold in effect. Now that we have this new function, we can use it here:

function GameRunner:spawn(isZombie)
    Player:instance():spawn(isZombie)
    self:requestWeaponsHold()
end

I think that makes a bit more clear what’s going on.

Let’s take a look at the main chunk we modified:

function Army:handleCycleEnd()
    if self.invaderNumber > #self.invaders then
        if self.invaderCount <= 0 then
            self:marshalTroops()
            Runner:requestWeaponsHold(2)
        end
        self.armySize = self.invaderCount
        self.invaderCount = 0
        self.invaderNumber = 1
        self:adjustMotion()
    end
end

Let’s extract a bit:

function Army:handleCycleEnd()
    if self.invaderNumber > #self.invaders then
        self:cycleEnd()
    end
end

function Army:cycleEnd()
    if self.invaderCount <= 0 then
        self:marshalTroops()
        Runner:requestWeaponsHold(2)
    end
    self.armySize = self.invaderCount
    self.invaderCount = 0
    self.invaderNumber = 1
    self:adjustMotion()
end

That should make it a bit more clear that we do cycleEnd when we’ve just done the last invader. What about that new method? Let’s try this:

function Army:handleCycleEnd()
    if self.invaderNumber > #self.invaders then
        self:cycleEnd()
    end
end

function Army:cycleEnd()
    self:handleEmptyRack()
    self.armySize = self.invaderCount
    self.invaderCount = 0
    self.invaderNumber = 1
    self:adjustMotion()
end

function Army:handleEmptyRack()
    if self.invaderCount <= 0 then
        self:marshalTroops()
        Runner:requestWeaponsHold(2)
    end
end

I was taught a principle, called “Composed Method” that says that method should either do things, or call things, but not both. So let’s extract the doing bit from cycleEnd:

function Army:handleCycleEnd()
    if self.invaderNumber > #self.invaders then
        self:cycleEnd()
    end
end

function Army:cycleEnd()
    self:handleEmptyRack()
    self:startRackUpdate()
end

function Army:handleEmptyRack()
    if self.invaderCount <= 0 then
        self:marshalTroops()
        Runner:requestWeaponsHold(2)
    end
end

function Army:startRackUpdate()
    self.armySize = self.invaderCount
    self.invaderCount = 0
    self.invaderNumber = 1
    self:adjustMotion()
end

Now I’m a bit worried about adjustMotion. That’s the thing that decides whether to step down, reverse, whatever:

function Army:adjustMotion()
    if self.overTheEdge then
        self.overTheEdge = false
        self.motion = self.motion*reverse + self.stepDown
    else
        self.motion.y = 0 -- stop the down-stepping
    end
end

I think we’re good here, but we should probably clear overTheEdge when we create a new rack, if that’s not already done somewhere. A search tells me that it isn’t. We should do it whenever we marshal new troops.

function Army:marshalTroops()
    self.invaders = MarshallingCenter():marshalArmy(self)
    self.overTheEdge = false
end

This is another belt-suspenders thing, but I think the logic requires it even if the timing would allow it to work.

I’m happy with this result. As usual, people uncomfortable with lots of tiny methods will be less comfortable. I’d urge them to try the approach for a while, until they can deal with it as well as whatever they’re doing now, and then decide which way they prefer. Or not. I’m not the boss of them.

Commit: refactor Army:handleCycleEnd.

What Now?

This has gone well. A test broke but then went green all on its own. No idea what that was, no good way to find out. Game plays, tests green now.

What else? Oh there was that thing about game over if the invaders get to the bottom. Let’s see what we can do about that.

The line across the bottom of the screen is at Y = 16:

function GameRunner:drawStatus()
    pushStyle()
    tint(0,255,0)
    sprite(self.line,8,16)
    ...

What about the shields? They are drawn at Y = 48:

function Shield:createShields()
    local img = readImage(asset.shield)
    local posX = 34
    local posY = 48
    local t = {}
    for s = 1,4 do
        local entry = Shield(img:copy(), vec2(posX,posY))
        table.insert(t,entry)
        posX = posX + 22 + 23
    end
    Shield.shields = t
end

So it seems to me that if the invaders Y is less than 48, it should be game over. Let’s try that. How will we know? We don’t want to search all the invaders for their Y value, surely.

Live invaders report for duty:

function Invader:update()
    self.pos = self.pos + self.army:invaderMotion() -- required for yPosition
    if not self.alive then return true end
    self.army:reportingForDuty()
    self.picture = (self.picture+1)%2
    Shield:checkForShieldDamage(self)
    if self:overEdge() then
        self.army:invaderOverEdge(self)
    end
    return false
end

What if we have them send in their Y coordinate and Army maintains the minimum value? We’ll have them send in their position:

function Invader:update()
    self.pos = self.pos + self.army:invaderMotion() -- required for yPosition
    if not self.alive then return true end
    self.army:reportingForDuty(self.pos)
    self.picture = (self.picture+1)%2
    Shield:checkForShieldDamage(self)
    if self:overEdge() then
        self.army:invaderOverEdge(self)
    end
    return false
end

In Army, we have:

function Army:reportingForDuty()
    self.invaderCount = self.invaderCount + 1
end

We have a gameOver function, which is called when lives run out. So to force game over we could set lives to zero somehow. Only the LifeManager really knows much about lives, though we do have this:

function GameRunner:drawStatus()
    pushStyle()
    tint(0,255,0)
    sprite(self.line,8,16)
    textMode(CORNER)
    fontSize(10)
    text("SCORE " .. tostring(TheArmy:getScore()), 144, 4)
    tint(0,255,0)
    local lives = self.lm:livesRemaining()
    text(tostring(lives), 24, 4)
    local addr = 40
    for i = 1,lives-1 do
        sprite(asset.play,40+16*i,4)
    end
    if lives == 0 then
        textMode(CENTER)
        text("GAME OVER", 112, 32)
    end
    drawSpeedIndicator()
    popStyle()
end

This rather messy method asks for and uses LM’s livesRemaining count. I think we want to tell GameRunner to tell LifeManager to consume all the lives. We’ll also need to explode the current player.

This is an experiment. If it works I plan to back it out and break.

First this:

function Army:reportingForDuty(invaderPos)
    if invaderPos.y < 48 then
        Runner:forceGameOver()
    end
    self.invaderCount = self.invaderCount + 1
end

Then this:

function GameRunner:forceGameOver()
    self.lm:forceGameOver()
    Player:instance():explode()
end

Then this:

function LifeManager:forceGameOver()
    self.life = #self.lives
end

I am just foolish enough to expect this to work. Curiously enough, it does!

Commit: invaders below shields = game over.

Enough already. Let’s sum up.

Summing Up

We got two new features today. A new rack of invaders appears when you manage to finish off a rack. And if the invaders get below the shield line, it’s instant Game Over.

We detected some temporal coupling in the game, where it works because certain objects do things in an order that other objects expect. We removed a bit of that by creating a new method that combined weapons hold and weapons hold time, but there’s still some of that going on.

However, temporal coupling isn’t all bad all the time. Imagine a game that consisted of an independent army of invaders marching and bombing, and independent gunners gunning, an independent life manager providing new gunners for a while, a game runner that just draw the situation on the board and gave everyone a chance to update.

There would be some necessary real communication between those objects. The invaders’ bombs would inform gunners that they were dead, the gunners’ missiles would inform the invaders (and the shields) of damage, and everything would just gracefully take care of itself.

That’s pretty much the situation during game play, and it works nicely and now that I think about it, I’m a bit proud of the fact that it does. But when you get to major “strategic” situations, like there being no army left or invaders landing, things get a bit messier.

They’re not terrible. They might be good enough, because the game is nearly done. But they’re not great, especially if we look toward a more complex situation. Suppose we wanted to build the two or three or more attract mode behaviors that the original game had. Suppose we wanted the easter egg where some text appears and an invader comes out and shoots it down. And so on.

As it stands, we’d look to GameRunner to handle that, and we might fall back on Main dealing with it. But presently those objects have a pretty weak handle on what’s going on. Mostly they just check to see if there are any lives left or some other detail.

I don’t have a solution in mind here, but I wonder whether there doesn’t need to be a sort of StrategicOverview object that keeps track of the whole situation, that can be interrogated, or that sends messages to other components to tell them how to behave. I can imagine that something like that would let us better see that high-level flow than what we have here.

If you look at the original assembly code, you won’t find a thing like that. You’ll find lots of branches asking questions like “is he exploding”, and no coherent chunk of code that looks like strategy. That’s how we often programmed in those days. We didn’t have much space to play with, and at least in my case, we didn’t know better than to do it that way.

Today, we have options, and part of what we do here is to use this tiny ancient program as a place to consider the options available to us today.

Or, in this case, tomorrow or Monday. See you then!

Invaders.zip