Displaying two player score. Beyond that, we’ll see.

I wasn’t planning to support two players, but I suppose that if we’re going to do that we should display both scores. I think we should put them both across the screen, way up top where no one ever goes. And if we make those values change dynamically, as who wouldn’t, we can remove the one from the bottom of the screen.

At least that’s my naive plan without looking at the code. Now to look at the code:

The active GameRunner draws status:

function GameRunner:drawStatus()
    pushStyle()
    tint(0,255,0)
    sprite(self.line,8,16)
    textMode(CORNER)
    fontSize(10)
    text("Player "..self.playerName.." SCORE " .. tostring(self.army:getScore()), 125, 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

Let’s extract the score-drawing bit. My reasons for that include:

  • We’re going to work on it, and this method does many things, and we like methods that do one thing;
  • I am thinking I’ll just ask both GameRunners to draw the score all the time. We’ll see if that works out.

So …

function GameRunner:drawStatus()
    pushStyle()
    tint(0,255,0)
    sprite(self.line,8,16)
    textMode(CORNER)
    fontSize(10)
    self:drawScore()
    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

function GameRunner:drawScore()
    text("Player "..self.playerName.." SCORE " .. tostring(self.army:getScore()), 125, 4)
end

This of course works just fine, and we can commit: refactor out drawScore. It’s a tiny pain to commit: I have to switch to the Working Copy task, press “commit”, paste in the commit message, press “all”, press “commit”, and come back. It’d be nice if it were even easier. I need all the help I can get. Still, a commit on something this trivial, that’s something to be proud of.

Now I want to shorten the message, change it to look a bit more like the old game, and move it to top left. The change is that the original game displays the player number as <1> or <2>. It displays, roughly:

SCORE <1>
  0000

I really don’t want to do that, because the spacing will be difficult. Therefore, let’s do it. Is there a leading zero format provided in whatever string formatting thing Codea uses? Probably. I’ll have to look it up.

Speaking of the name, I’m a bit concerned about my trick renaming the zombie player’s GameRunner to Z. That clobbers the name in GameRunner. Cute trick but it’s going to break this feature. We may have to do something different about that.

Is this the price of cleverness? Perhaps it is. Anyway, first cut:

function GameRunner:drawScore()
    local output = "Score <"..self.playerName..">\n" .. tostring(self.army:getScore())
    sx,sy = textSize(output)
    text(output, 8, 256-sy)
end

This gives me this look:

score

Not bad. Needs to be all upper case, and we need that leading zero formatting that was so hot back then.

I refactor to this:

function GameRunner:drawScore()
    local score = tostring(self.army:getScore()
    local output = "SCORE <"..self.playerName..">\n" .. score)
    sx,sy = textSize(output)
    text(output, 8, 256-sy)
end

Now the last time I did something like what I’m about to do, Dave1707 of Codea forum fame, schooled me about the fact that Codea has c-style formatting built in. I’m going to do it anyway.

function GameRunner:drawScore()
    local score = "0000" .. tostring(self.army:getScore())
    score = score:sub(-4,-1)
    local output = "SCORE <"..self.playerName..">\n    " .. score
    sx,sy = textSize(output)
    text(output, 8, 256-sy)
end

Now we have this:

score centered

So that’s nice. What about the other score? I have this in mind:

function GameRunner:drawStatus()
    pushStyle()
    tint(0,255,0)
    sprite(self.line,8,16)
    textMode(CORNER)
    fontSize(10)
    Player1:drawScore()
    Player2:drawScore()
    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

Now if drawScore were just smart enough to draw in two locations, we’d be good. I am minded to let the function decide, although we could also pass the x origin in from here. But, you see, I have a trick in mind. When it’s a one player game, Player1 = Player2.

Nah. Too clever by half. We’ll do this:

function GameRunner:drawStatus()
    pushStyle()
    tint(0,255,0)
    sprite(self.line,8,16)
    textMode(CORNER)
    fontSize(10)
    self:drawScores()
    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

function GameRunner:drawScores()
    Player1:drawScore(8)
    if Player1 ~= Player2 then
        Player2:drawScore(224-60)
    end
end

function GameRunner:drawScore(x)
    local score = "0000" .. tostring(self.army:getScore())
    score = score:sub(-4,-1)
    local output = "SCORE <"..self.playerName..">\n    " .. score
    sx,sy = textSize(output)
    print(sx)
    text(output, x, 256-sy)
end

I’m still too clever for my shoes. Since Player1 and Player2 change positions back and forth, the scores at the top display back and forth. So this is a good idea but not quite a great one.

However:

function GameRunner:drawScores()
    Player1:drawScore()
    if Player1 ~= Player2 then
        Player2:drawScore()
    end
end

function GameRunner:drawScore()
    local x = 8
    if self.playerName == "2" then
        x = 224-60
    end
    local score = "0000" .. tostring(self.army:getScore())
    score = score:sub(-4,-1)
    local output = "SCORE <"..self.playerName..">\n    " .. score
    sx,sy = textSize(output)
    print(sx)
    text(output, x, 256-sy)
end

This give us the desired result during play:

two scores

Wow, I should head out for my chai. Maybe I can miss the school buses. Commit: two scores display. BRB.

Well, that took a bit longer than anticipated: I didn’t miss the school buses. Did you know that the plural of bus is buses. I did miss the busses, if there were any.

Now the scores look good during game play, but when the game drops into attract mode odd things happen. I think we should just not display score when the zombies are playing. We don’t have a direct way to check that but we do have these two methods:

function GameRunner:spawningZombie()
    self.playerName = "Z"
    self.army:zeroScore()
    self:soundPlayer():setVolume(0.05)
end

function GameRunner:spawningNormal()
    self:soundPlayer():setVolume(1.0)
end

That seems convenient. How about adding a status flag in GameRunner, like this:

function GameRunner:spawningZombie()
    self.runningZombie = true
    self.playerName = "Z"
    self.army:zeroScore()
    self:soundPlayer():setVolume(0.05)
end

function GameRunner:spawningNormal()
    self.runningZombie = false
    self:soundPlayer():setVolume(1.0)
end

And then do this:

function GameRunner:drawScores()
    if self.runningZombie then return end
    Player1:drawScore()
    if Player1 ~= Player2 then
        Player2:drawScore()
    end
end

That should do the trick. With an init to true in GameRunner init it does. I think I’d like to move the text down about a half line, so:

function GameRunner:drawScore()
    local x = 8
    if self.playerName == "2" then
        x = 224-60
    end
    local score = "0000" .. tostring(self.army:getScore())
    score = score:sub(-4,-1)
    local output = "SCORE <"..self.playerName..">\n    " .. score
    sx,sy = textSize(output)
    print(sx)
    text(output, x, 252-sy)
end

That looks good. Commit: player scores shown at top. Ship it.

Shots not completing

I think I’ve noticed something. There is only one missile, and so you’re not supposed to be able to fire until the running missile has finished its run. However, in rapid fire, when you hit a falling bomb, it appears that rapid tapping will fire too soon, literally calling the missile back to begin again (since there is only one).

Let’s see if we can figure out whether this is happening, ad if so, why. And fix it, that goes without saying except that I just said it.

When the game is running, touches go straight to the Gunner:

function Gunner:touched(touch)
    local fireTouch = WIDTH-195
    local moveLeft = 97
    local moveRight = 195
    local moveStep = 1.0
    local x = touch.pos.x
    if touch.state == ENDED then
        self.gunMove = vec2(0,0)
        if x > fireTouch then
            self:fireMissile()
        end
    end
    if touch.state == BEGAN or touch.state == CHANGED then
        if x < moveLeft then
            self.gunMove = vec2(-moveStep,0)
        elseif x > moveLeft and x < moveRight then
            self.gunMove = vec2(moveStep,0)
        end
    end
end

So that leads to this:

function Gunner:fireMissile()
    if (self.count == 0 and (self.alive or self.zombie)) and self.missile.v == 0 then
        self:unconditionallyFireMissile()
    end
end

function Gunner:unconditionallyFireMissile(silent)
    self.missile.pos = self.pos + vec2(7,5)
    self.missile.v = 1
    self.missileCount = self.missileCount + 1
    if not silent then Runner:soundPlayer():play("shoot") end
end

It seems that we’ll fire only if the missile isn’t moving, i.e. missile.v is zero. I wonder if the short missile is hitting an invisible target, perhaps a bomb that is being destroyed or something like that.

This looks a bit odd:

function Missile:draw()
    if self.v == 0 then return end
    pushStyle()
    if self.explodeCount > 0 then
        tint(self:explosionColor())
        sprite(self.explosion, self.pos.x - 4, self.pos.y)
        self.explodeCount = self.explodeCount - 1
        if self.explodeCount == 0 then
            self.v =  0
        end
    else
        rect(self.pos.x, self.pos.y, 2,4)
    end
    popStyle()
    if self.v > 0 then Runner.army:processMissile(self) end
end

If the missile is exploding, it displays its explosion and counts down self.explodeCount. Only when that gets to zero does it set v to zero. That seems OK. But at the bottom, we call processMissile …. even if we’re exploding. That seems wrong. Let’s move that:

function Missile:draw()
    if self.v == 0 then return end
    pushStyle()
    if self.explodeCount > 0 then
        tint(self:explosionColor())
        sprite(self.explosion, self.pos.x - 4, self.pos.y)
        self.explodeCount = self.explodeCount - 1
        if self.explodeCount == 0 then
            self.v =  0
        end
    else
        rect(self.pos.x, self.pos.y, 2,4)
        Runner.army:processMissile(self)
    end
    popStyle()
end

I’ll try that for game play.

I definitely saw the short fire happen that time, and the second missile may have actually disappeared before I fired. That is, maybe it’s not firing too soon, but the prior missile is dying too soon. But if so, why would it not show its explosion?

At a random guess: the second “short” missile is hitting a bomb that has already been destroyed but hasn’t left the screen.

We find this:

function Bomb:processMissile(missile)
    if self:killedBy(missile) then
        missile.v = 0
        self:explode()
    end
end

And this:

function Bomb:killedBy(missile)
    return rectanglesIntersect(self.pos,3,4, missile.pos,3,4)
end

We aren’t checking to see if the bomb is alive. (We also do not trigger the missile explosion, which may be an issue, though I think I decided not to do that to avoid two explosions at the same point.) So an exploding dead bomb could stop a missile.

function Bomb:explode()
    self.pos = self.pos - vec2(2,3)
    self.explodeCount = 15
end

And …

function Bomb:draw()
    pushStyle()
    if self.explodeCount > 0 then
        sprite(self.explosion, self.pos.x, self.pos.y)
        self.explodeCount = self.explodeCount - 1
        if self.explodeCount == 0 then self.alive = false end
    else
        sprite(self.shapes[self.shape + 1],self.pos.x,self.pos.y)
    end
    popStyle()
end

We don’t set alive to false until explodeCount is zero. Therefore we need something like this:

function Bomb:processMissile(missile)
    if self:isKillable() and self:killedBy(missile) then
        missile.v = 0
        self:explode()
    end
end

With this:

function Bomb:isKillable()
    return self.alive and self.explodeCount <= 0
end

I reckon that will make that little problem go away. It’s hard to duplicate but I’ll play a while to gain confidence. Then let’s talk about testing.

I saw some solid hits on bombs, followed by my next shot going past that location. I think we’ve found and fixed that defect. Commit: fixed defect causing shots to stop at dying bomb locations.

Now to sum up including some thoughts on testing.

Could tests have found that defect?

What would a test look like to detect this defect? We’d have to ask ourselves the question, and I think that’s the hard part. What is the question?

Is it “After a bomb has been killed by a missile, can another missile hit it”? It’d be hard to think of that.

Is it “After a bomb has been killed by a missile, can another missile hit the debris”? That, at least, makes a kind of real world sense, but not much in our world. Except that it happened.

It is “After a bomb has been killed, can it be killed again”? That seems like such a silly question that if we thought of it, we’d laugh and toss it out.

What about “When are things killable”? That’s an interesting question and we have some odd code in the system now that is answering that question without expressing it.

function Invader:killedBy(missile)
    if not self.alive then return false end
    if self:isHit(missile) then
        self.alive = false
        self.army:addToScore(self.score)
        self.exploding =15
        Runner:soundPlayer():play("killed")
        return true
    else
        return false
    end
end

This code answers a different question, but one that happens to be valid: is the Invader alive? Since we immediately mark her not alive if she’s hit, this is a valid test.

But we don’t handle the bombs the same way. There, we don’t mark them as not alive until their explosion is over. Thus our more complex definition of isKillable, which we just coded up.

For completeness, let’s look at the saucer:

function Saucer:processMissile(missile)
    if self.alive and self:isHit(missile) then
        self:die()
        missile.v = 0
    end
end

function Saucer:die()
    self.alive = false
    local sc = self:score()
    self.army:addToScore(sc)
    self.textScore = string.format("%d",sc)
    self.exploding = 60
    Runner:soundPlayer():play("killed")
end

This continues the notion that alive is reset immediately. So our Bomb is the odd man out.

In a perfect world, we’d bring the Bomb into line with the others, and we might even observe some duplication across the classes and see about removing it. Today we do not live in a perfect world, and I’m not going to attempt that trick.

But what about the testing aspect? If they all followed the rule of becoming not alive immediately, we could at least imagine a test that set each against a missile and checked to see that it had set alive to false. That’s a test of game state that I could imagine doing. But I didn’t think of that at the time, at least in part because when I’m just doing one, a test seems redundant when I just set alive to false a second ago.

A better person might have tested that. I’m not sure if a better person would close the barn door with a test now, or not. For this morning, the answer for me is “not”. I think what we have here is a design flaw, not having a common way to handle the alive-dead topic, which has resulted in simple code, used somewhat inconsistently, and leading to a situation where tests just don’t come to mind.

In short, I messed up, as one does, but in a way that was hard for me to spot. A pair, or a mob,, might have spotted it. Here, the cat and I did not.

Otherwise …

Otherwise, the morning has gone very nicely. We made the one- and two-player scores appear at top of screen, formatted much like the original game, and we found and fixed a curious little defect of timing. We never got deeply confused, and our fingers never left our hands.

A good morning. And a good morning to you. See you next time.

Invaders.zip