I don’t feel very bright this morning. Let’s see if I can prove it.

I’ve got my chai, and started drinking it. Maybe I’ll come up to speed. We’ll see.

My main plan today is to clean up some of the code. I think I”ll also do a small adjustment to the buttons, because their slight overlap with the top row of invaders bugs me. It also bugs Dave1707, who sent me a patch to do it. He reports that the game plays better on his iPhone 8 than on his iPad. That’s interesting, and it’s good to know that the game gets played.

I’d like to convert it to a real iOS app, but I have no experience doing that, and I’d want someone to pair with me who knew the ropes. I’m sure there are a lot of ropes to know. Anyway, for now, the buttons.

Even before that, I’ve been meaning to start the game in full screen mode. I prefer having the Codea display overlay visible, because I’m more likely to be debugging than I am playing, but my few users are there to play, not debug. There aren’t many bugs anyway.

function draw()
    pushMatrix()
    pushStyle()
    viewer.mode = FULLSCREEN
    background(40, 40, 50)
    showTests()
    ...

Commit: Game starts in full screen mode.

Now the buttons, which look like this:

buttons

The code is this:

function defineButtons()
    local y = Constant:saucerY() - 4
    b1 = Button:leftButton("Touch for\nOnePlayer", 10, 8, y-4)
    b2 = Button:rightButton("Touch for\nTwo Players", 10, 216, y-4)
end

Now Dave said something about the alignment of the boxes with the line at the bottom, so let’s move the buttons down for a quick comparison.

His code was this:

   b1 = Button:leftButton(" Touch for\nOnePlayer", 10, 6, y+4)
   b2 = Button:rightButton("  Touch for\nTwo Players", 10, 208, y+4)

So he changed the start to x = 6 on the left one and the finish to x = 216 on the right one. He also added some spacing to better center the text. I think we have a text align mode that can do that. We’ll see. First, I’ll patch y to bring the buttons down near the line, which appears at (8,16).

button low

Sure enough, we are overlapping on the right. I’m not sure why he nudged the left one left but I’ll try both numbers.

I’m buying the 208, but not the 6.

function defineButtons()
    local y = Constant:saucerY() - 4
    b1 = Button:leftButton("Touch for\nOnePlayer", 10, 8, y-4)
    b2 = Button:rightButton("Touch for\nTwo Players", 10, 208, y-4)
end

Commit: reposition buttons for better alignment.

Now let’s see about centering the text:

function drawButtons()
    b1:draw()
    b2:draw()
end

That becomes …

function drawButtons()
    pushStyle()
    textAlign(CENTER)
    b1:draw()
    b2:draw()
    popStyle()
end

That gives us this:

button text centered

Now for that overlap. Dave’s solution was to move the boxes up a bit. His version doesn’t display the tests, so he can tolerate them moving up a full line, which is what he did, changing y from -4 to +4. My plan is to move up just a bit, but also to squeeze in the box frames:

Ha. Looking at this code with my eyes open:

function defineButtons()
    local y = Constant:saucerY() - 4
    b1 = Button:leftButton("Touch for\nOnePlayer", 10, 8, y-4)
    b2 = Button:rightButton("Touch for\nTwo Players", 10, 208, y-4)
end

I wonder why I didn’t just say -8 at the top. Let’s do that along the way. I’m going to go into Button, though, and change the margins and spacing:

function Button:init(message, size, direction, boxInputX, boxY)
    local margin = 8
    local spacing = 4
    self.message = message
    ...

Let’s halve those:

function Button:init(message, size, direction, boxInputX, boxY)
    local margin = 4
    local spacing = 2
    ...

That makes the margins smaller but of course the box stays at the same y height. Now then:

function defineButtons()
    local y = Constant:saucerY() - 4
    b1 = Button:leftButton("Touch for\nOnePlayer", 10, 8, y)
    b2 = Button:rightButton("Touch for\nTwo Players", 10, 208, y)
end

Note that I removed the -4s in the two creation lines, which moves the buttons up 4 pixels, which looks good to me:

buttons good

Commit: Buttons resized and repositioned.

OK, we have a user-requested feature for the day. Not a very exciting one. That reminds me to look at the sticky note, however. It also says “Make saucer less frequent”. Let’s take a look at that.

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

function Army:runSaucer()
    if self.saucer:isRunning() then return false end
    if self:yPosition() > Constant:saucerRunningLimit() then return false end
    if math.random(1,20) < 10 then return false end
    self.saucer:go(Gunner:instance():shotsFired())
    return true
end

The main decider here is this:

    if math.random(1,20) < 10 then return false end

Half the time when we try to run the saucer, if it isn’t already running, we’ll run it. This will take some tuning. I’ll start by making it 10 times harder to run:

    if math.random(1,200) > 10 then return false end

I reversed the sense of the if, and multiplied the random range by 10. I also tried a range of 100. I don’t like the behavior in either case. Sometimes it waits too long, sometimes it repeats almost as frequently as with the current setting. I think we’re going to need to change how this works, probably to a variable time delay around some central value. Let’s see if we can find out what the original game does. If I recall, it’s kind of arcane.

Well, it’s not so bad if we ignore the stuff about sharing a slot with the squiggly shot.

;##-TimeToSaucer
0913: 3A 09 20        LD      A,($2009)           ; Reference alien's X coordinate
0916: FE 78           CP      $78                 ; Don't process saucer timer ... ($78 is 1st rack Yr)
0918: D0              RET     NC                  ; ... unless aliens are closer to bottom
0919: 2A 91 20        LD      HL,($2091)          ; Time to saucer
091C: 7D              LD      A,L                 ; Is it time ...
091D: B4              OR      H                   ; ... for a saucer
091E: C2 29 09        JP      NZ,$0929            ; No ... skip flagging
0921: 21 00 06        LD      HL,$0600            ; Reset timer to 600 game loops
0924: 3E 01           LD      A,$01               ; Flag a ...
0926: 32 83 20        LD      ($2083),A           ; ... saucer sequence
0929: 2B              DEC     HL                  ; Decrement the ...
092A: 22 91 20        LD      ($2091),HL          ; ... time-to-saucer
092D: C9              RET                         ; Done

This code says that the saucer can run every 600 game cycles. but there’s more in the notes: the saucer can’t run if the squiggly shot is running. We aren’t handling things that way at all. And right now, that’s not the story we’re on. Still, maybe we can pick it up. Let’s look a bit harder at how all this works:

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

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

We use a single “bomb cycle limit” to decide whether we can drop a bomb. That value is variable, based on the player’s score. We don’t need to worry about that.

In dropBomb we have this:

function Army:dropBomb(aBomb)
    if aBomb.alive then return end
    local col = aBomb:nextColumn(self)
    local bombPos = self:bombPosition(col)
    if bombPos ~= nil then
        aBomb.pos = bombPos
        self.bombCycle = 0
        aBomb.alive = true
    end
end

This ensures that we only drop a bomb when it’s not running, and that we only have one of a given type on screen at a time. If we wanted to ensure that we didn’t run the saucer when we’re running the squiggle bomb, we could check to see if it’s alive in the saucer logic.

Why not?

function Army:runSaucer()
    if self.saucer:isRunning() or self.squiggleBomb.alive then return false end
    --if self:yPosition() > Constant:saucerRunningLimit() then return false end
    if math.random(1,100) > 10 then return false end
    self.saucer:go(Gunner:instance():shotsFired())
    return true
end

That seems reasonable. Should be safe since we check for the saucer first, so it should still find times when the squiggle isn’t running. Now what about the saucer cycle?

Before I dig into that, I want to run this code, with the saucer odds set back to where they were, just to be sure it can still run … and it can. Now we have this thing called bombCycle that is incremented on update:

function Army:updateBombCycle(anAmount)
    self.bombCycle = self.bombCycle + (anAmount or 1)
end

We can add saucerCycle and use it.

function Army:updateBombCycle(anAmount)
    self.bombCycle = self.bombCycle + (anAmount or 1)
    self.saucerCycle = self.saucerCycle + 1
end

(And set to zero when we init. I’ll spare you that.)

Now we change runSaucer:

function Army:runSaucer()
    if self.saucer:isRunning() or self.squiggleBomb.alive then return false end
    --if self:yPosition() > Constant:saucerRunningLimit() then return false end
    if math.random(1,20) > 10 then return false end
    self.saucer:go(Gunner:instance():shotsFired())
    return true
end

(By the way, the commented line just makes the saucer start right away. Convenient for testing.)

function Army:runSaucer()
    if self.saucer:isRunning() or self.squiggleBomb.alive then return false end
    --if self:yPosition() > Constant:saucerRunningLimit() then return false end
    if self.saucerCycle < 600 then return false end
    self.saucer:go(Gunner:instance():shotsFired())
    return true
end

Now we need to reset saucerCycle. I’d rather like to do it in the saucer, but the connection to Army is a bit iffy So we’ll set to zero here …

function Army:runSaucer()
    if self.saucer:isRunning() or self.squiggleBomb.alive then return false end
    --if self:yPosition() > Constant:saucerRunningLimit() then return false end
    if self.saucerCycle < 600 then return false end
    self.saucerCycle = 0
    self.saucer:go(Gunner:instance():shotsFired())
    return true
end

But wouldn’t it be better to reset the time when the saucer dies, not just when it starts? We don’t really have that information. The saucer can die from being hit, in which case it does call back to army, or it can just time out:

function Saucer:update()
    if not self.alive then return end
    local newPos = self.pos + self.step
    if self.startPos.x < self.stopPos.x and newPos.x <= self.stopPos.x then
        self.pos = newPos
    elseif self.startPos.x > self.stopPos.x and newPos.x >= self.stopPos.x then
        self.pos = newPos
    else
        self.alive = false
    end
end

When it runs over the edge, it just quietly sets alive to false. And the army is watching that flag. Kind of old-school, that. For now, though, I’m not going to refactor that: we have our implementation hat on.

What we have now should give us about ten seconds between saucer runs, if I’m not mistaken. (I am frequently mistaken, as you may have noticed.)

It seems a bit faster than that to me, but I’m sure it’s updating on 60ths. It’s certainly far less often than it was, so I’m going to let it ride.

Let’s move that magic number 600 though.

function Constant:saucerCycleLimit()
    return 600 -- nominal ten seconds
end

function Army:runSaucer()
    if self.saucer:isRunning() or self.squiggleBomb.alive then return false end
    if self:yPosition() > Constant:saucerRunningLimit() then return false end
    if self.saucerCycle < Constant:saucerCycleLimit() then return false end
    self.saucerCycle = 0
    self.saucer:go(Gunner:instance():shotsFired())
    return true
end

Letting the game run, I noticed an interesting bug. Or two. When the game is over, the zombie player starts playing where the dying player left off, with a partial rack. That’s not what I expected, but it might be OK. However, if invaders in the rack make it to the bottom, the zombie player explodes as we expect, but we don’t get a new rack. Let’s see where that decision is made:

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

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

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

I think someone should be setting the invaderCount to zero here, or otherwise causing a new rack. That’s done here:

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

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

I think that check for y needs to be this:

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

This is a bit magical, triggering such an important notion as a new rack by setting a count but that’s how it works. This will take a while to test fully but my first test makes me think it works OK.

I think we can commit: fixed game over behavior when invaders hit bottom to marshal new troops.

Oh, bad commit message, we had that feature of slowing down the saucer too. Bad Ron. I don’t think Working Copy will let me change the commit message. We’ll just deal with it.

It’s time to sum up.

Session Retrospective

We did nothing but add and adjust little features this morning. Didn’t get to doing any code cleanup at all. This is the sort of thing that happens all too easily, when we are focused on features. We let the internal code quality slide. We think we’ll get to it on Monday, but Monday comes and we do a few little feature fixes instead. Slowly the code quality deteriorates.

And we did see some areas that are questionable.

I like the basic operation of the game, where the different entities mostly take care of themselves, and communicate among each other when they need to. But sometimes that communication is implicit, like the Army deciding to marshall new troops based on the invader count changing. As soon as I wrote that line that zeroed the count, the code was obfuscated. It used to be that invaderCount was the count of invaders. Now, suddenly, it isn’t quite that. It’s almost always the invader count but sometimes it’s a flag saying we want the count to be zero.

I’m famous for saying that code never lies but comments sometimes do, but here we have a case where the code doesn’t lie … because it does just what it says … but what it says is hard to understand.

The code doesn’t lie, but it can try to confuse us. Or, more accurately, we try something that confuses us. The code is innocent: it’s the guy who hammered that value who did the confusing.

What we have here is a subtle interaction inside of Army. At least it’s not exported to others, but it’s still a bit weird.

Let’s try to look at that tomorrow. For today, our session is over. Everything works a bit better than it did before. See you soon!

Invaders.zip