Attract mode needs to be turned back on. Maybe we should show both players’ scores. What about those buttons?

I was thinking this morning, as I ran hot water on myself, whether writing these articles concurrently with programming makes the programming go better, or worse. And if it makes the programming go better, what might that mean to someone who doesn’t want to write programs and articles at the same time.

On the one hand, it is distracting to have to pick up my head from programming every few minutes, to type a few paragraphs about what I’m thinking. On the other hand, typing about what I’m thinking requires me to make some sense of my thoughts, lest I type something like “whathe #@%# gol durn #$5# is even happening”, which is often approximately correct. Making sense of my thoughts then helps when I then go heads back down. Well, head, as I only have one.

On the gripping hand, I think one would get similar assistance when pairing and talking aloud about what’s going on. But for a solo programmer, I suspect that something that makes them raise their head frequently and get their thoughts in order would be desirable.

Then I grabbed a towel. Then I went for chai. Then I came home. Then, we were right here.

Attract Mode

The game properly goes into attract mode when a game completes, but it does not start in attract mode any more. Attract mode works by a very simple trick: the LifeManager, owned by the Player (a GameRunner instance), returns a zombie player after all the lives are consumed. Controls are ignored, and the zombie goes about its business.

We new create two real players at startup:

function setup()
    runTests()
    parameter.boolean("Cheat", false)
    Runner = nil
    Player1 = GameRunner(3, "1")
    Player2 = GameRunner(3, "2")
end

function startGame(touch)
    Runner = Player1
    if touch.x < WIDTH/2 then
        Player2 = Player1
    end
    invaderNumber = 1
end

function touched(touch)
    if Runner and Runner:livesRemaining() ~= 0 then
        Gunner:instance():touched(touch)
    else
        if touch.state == ENDED then
            testsVisible(false)
            startGame(touch)
        end
    end
end

We used to initialize with a Runner that had been initialized with zero lives, which meant that it only spawned zombies. Thus, initial attract mode. To replicate this behavior, we’re going to have to move things around a bit.

This should start us in attract mode:

function setup()
    runTests()
    parameter.boolean("Cheat", false)
    Player1 = GameRunner(0,"z")
    Player2 = Player1
    Runner = Player1
end

And it does.

attract

However, now when you press the buttons, the game doesn’t start, because startGame expects that things are set up in Player1 and Player2. So we need to change that. Some good news is that there is only one caller of startGame, and it’s main, so we can change it without worrying about other users.

I wonder if one could declare something like that local. If so, the code could express that no one else uses it. I guess I’ll try it.

local function startGame(touch)
    Player1 = GameRunner(3, "1")
    Runner = Player1
    if touch.x < WIDTH/2 then
        Player2 = Player1
    else
        Player2 = GameRunner(3, "2")
    end
    invaderNumber = 1
end

Here we create a new GameRunner, or two if needed, wiping out the one created in setup. We don’t really need to worry about the game over state, because it keeps playing with a zombie.

(There is an issue, which is that the score appears to apply to whichever player ended the game. We should add to our list to set the game name to Z when we enter zombie mode.)

((Which brings up another issue. At my desk, despite its great expanse, I have not provided a place to make notes about things we “should” do. The result is that I forget. I don’t usually worry about that, because sooner or later the idea will come up again, but it would be nice to have a notecard of some kind. A card would get lost in the clutter. I’ll try a mid-sized sticky note, which I can move to the more open area when it fills up.))

(((I mention this for two reasons. First, it caused me to get out a stack of sticky notes. Second, you, too, may have need of remembering things, and maybe this reminder will remind you.)))

Let’s test the above code. I expect it to work.

Works as intended. I notice for the thousandth time that the Saucer flies far too often, which is good for testing but not for playing. Hey, I have a sticky note here to write on. Also, if you hit the saucer, the sound can double, because it doesn’t stop when the saucer dies, it continues until the wav file runs out. Note made of that, too.

Now about the player score. But first a commit: game starts in attract mode.

I’d like zombie players to set their player name to “Z”. It would be nice if they zeroed out the score as well. Right now however, no one really does anything about zombies appearing. They just show up. There are references to the zombie flag. Ah, here’s a nice opportunity:

function Gunner:spawn(isZombie, pos)
    self.zombie = isZombie or false
    if self.zombie then
        Runner:soundPlayer():setVolume(0.05)
    else
        Runner:soundPlayer():setVolume(1.0)
    end
    self.zombieCount = 0
    self.zombieMotion = 0
    self.alive = not self.zombie
    self.drawingStrategy = self.drawPlayer
    self:setPos(pos)
end

Perfect, we can do it right in that if block. We’ll tell the Runner we’re a zombie and he can do the rest. The first pass goes like this:

function Gunner:spawn(isZombie, pos)
    self.zombie = isZombie or false
    if self.zombie then
        Runner:spawningZombie()
        Runner:soundPlayer():setVolume(0.05)
    else
        Runner:soundPlayer():setVolume(1.0)
    end
    self.zombieCount = 0
    self.zombieMotion = 0
    self.alive = not self.zombie
    self.drawingStrategy = self.drawPlayer
    self:setPos(pos)
end

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

And now I need this:

function Army:zeroScore()
    self.score = 0
    self:adjustTiming(self.score)
end

However, clearly Runner should be adjusting the sound for the zombie not the Gunner. That says we need spawnNormal, so we wind up with this:

function Gunner:spawn(isZombie, pos)
    self.zombie = isZombie or false
    if self.zombie then
        Runner:spawningZombie()
    else
        Runner:spawningNormal()
    end
    self.zombieCount = 0
    self.zombieMotion = 0
    self.alive = not self.zombie
    self.drawingStrategy = self.drawPlayer
    self:setPos(pos)
end

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

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

And that works as intended. After the player’s last Gunner dies, the zombie appears and the score shows correctly:

zombie score

Commit: zombies play as player “Z”.

There’s a side benefit to having created the spawnNormal function, I think. It makes the way things work just a bit more obvious. Why would there be a spawnZombie and no spawnNormal? We’d wonder. And it has something to do, so it isn’t just sitting there waiting to be filled in.

I think that it often happens that when we respond to a little whisper like the one here, we wind up with code that will serve us better in the long run. Having two calls to Runner in the Gunner if block was a tiny bit of feature envy, so we built the feature into GameRunner instead. We did that in two passes, because we needed to create the two separate methods. Yes, it could have been done in one pass, but I felt this way was safer.

I noticed something that I don’t like as the zombie played. The saucer flies behind the Touch to Start buttons. Our Main draw looks like this:

function draw()
    pushMatrix()
    pushStyle()
    background(40, 40, 50)
    showTests()
    if Runner then Runner:draw() end
    if not Runner or Runner:livesRemaining() == 0 then
        drawTouchToStart()
    end
    popStyle()
    popMatrix()
    if Runner then Runner:update() end
end

We draw the touch to start after drawing the Runner. If we swap those, it should be no harm done.

function draw()
    pushMatrix()
    pushStyle()
    background(40, 40, 50)
    showTests()
    if not Runner or Runner:livesRemaining() == 0 then
        drawTouchToStart()
    end
    if Runner then Runner:draw() end
    popStyle()
    popMatrix()
    if Runner then Runner:update() end
end

And now the saucer (and missiles) go in front of the buttons:

in front

Commit: saucers before buttons.

Maybe One More Thing

I feel that I’ve done a good morning’s work here, having spent at least an hour and 15 minutes doing this, but maybe we could do one more thing. The blurb says:

Attract mode needs to be turned back on. Maybe we should show both players’ scores. What about those buttons?

I don’t quite want to do the player score thing, though it should be easy enough. The buttons are a bit prominent now, as you’ll have noticed in the picture above. That’s mostly because they have a black background. In addition, they just slightly overlap the top row of aliens, which looks weird. Let’s change the background, squeeze the box in a bit, and shift it up just a skosh.

While we’re at it, we have a lot of duplication going on here:

function drawTouchToStart()
    pushMatrix()
    pushStyle()
    stroke(255)
    strokeWidth(1)
    fill(255)
    fontSize(10)
    textMode(CORNER)
    rectMode(CORNER)
    GameRunner:scaleAndTranslate()
    local s1 = "Touch for\nOne Player"
    local s2 = "Touch for \nTwo Players"
    local y = Constant:saucerY() - 4
    local sx,sy = textSize(s1)
    fill(0)
    rect(8,y-4, 16+sx, sy+8)
    sx,sy = textSize(s2)
    rect(208-sx, y-4, 16+sx, sy+8)
    fill(255)
    text("Touch for\nOne Player", 16, y)
    x = 216-textSize("Two Players")
    text("Touch for\nTwo Players", x, y)
    popStyle()
    popMatrix()
end

I probably should have put a trigger warning ahead of that code, shouldn’t I? Let’s break this out a bit. And keep an eye on the steps, I think this will be interesting.

function drawTouchToStart()
    pushMatrix()
    pushStyle()
    stroke(255)
    strokeWidth(1)
    fill(255)
    fontSize(10)
    textMode(CORNER)
    rectMode(CORNER)
    GameRunner:scaleAndTranslate()
    drawButtons()
    popStyle()
    popMatrix()
end

function drawButtons()
    local s1 = "Touch for\nOne Player"
    local s2 = "Touch for \nTwo Players"
    local y = Constant:saucerY() - 4
    local sx,sy = textSize(s1)
    fill(0)
    rect(8,y-4, 16+sx, sy+8)
    sx,sy = textSize(s2)
    rect(208-sx, y-4, 16+sx, sy+8)
    fill(255)
    text("Touch for\nOne Player", 16, y)
    x = 216-textSize("Two Players")
    text("Touch for\nTwo Players", x, y)
end

This is a pure cut and paste, so unless I’ve skipped a variable, it should work as before. And it does. Now we have two buttons going on here. How about if we do first one and then the other?

Wait, first. Yesterday I created s1 and s2 but didn’t use them in the text. I meant to and forgot. So:

function drawButtons()
    local s1 = "Touch for\nOne Player"
    local s2 = "Touch for \nTwo Players"
    local y = Constant:saucerY() - 4
    local sx,sy = textSize(s1)
    fill(0)
    rect(8,y-4, 16+sx, sy+8)
    sx,sy = textSize(s2)
    rect(208-sx, y-4, 16+sx, sy+8)
    fill(255)
    text(s1, 16, y)
    x = 216-textSize("Two Players")
    text(s2, x, y)
end

Now I have learned that textSize works correctly for text with newlines in it, so we don’t really need to call textSize at the bottom there, it’ll be sx.

function drawButtons()
    local s1 = "Touch for\nOne Player"
    local s2 = "Touch for \nTwo Players"
    local y = Constant:saucerY() - 4
    local sx,sy = textSize(s1)
    fill(0)
    rect(8,y-4, 16+sx, sy+8)
    sx,sy = textSize(s2)
    rect(208-sx, y-4, 16+sx, sy+8)
    fill(255)
    text(s1, 16, y)
    x = 216-sx
    text(s2, x, y)
end

Three tiny changes, slight improvements, all works. Now grouping:

function drawButtons()
    local s1 = "Touch for\nOne Player"
    local y = Constant:saucerY() - 4
    local sx,sy = textSize(s1)
    fill(0)
    rect(8,y-4, 16+sx, sy+8)
    fill(255)
    text(s1, 16, y)
    
    local s2 = "Touch for \nTwo Players"
    sx,sy = textSize(s2)
    fill(0)
    rect(208-sx, y-4, 16+sx, sy+8)
    x = 216-sx
    fill(255)
    text(s2, x, y)
end

Here I had to duplicate the fill(0) and fill(255), because text color is controlled by fill. Now we seem to have a lot of duplication here, but I don’t quite see what to do about that x calculation.

Let’s see. We want both of these guys to start at the same y, so that’s a parameter to any function we write. We want to control the x as well, but we want to right justify our second case and not the first. Hm. That gives me a half an idea. What if the right-justifying one calculated the x and called the left-justifying one?

Let’s try that. First the left one. Something like this:

function drawButtons()
    local y = Constant:saucerY() - 4
    leftButton("Touch for\nOne Player", 8, y-4)
    
    local s2 = "Touch for \nTwo Players"
    sx,sy = textSize(s2)
    fill(0)
    rect(208-sx, y-4, 16+sx, sy+8)
    x = 216-sx
    fill(255)
    text(s2, x, y)
end

And we’ll then want something like this:

function leftButton(message, x, y)
    local sx,sy = textSize(message)
    fill(0)
    rect(x,y-4, 16+sx, sy+8)
    fill(255)
    text(message, x+4, y)
end

That’s not quite right, as the picture shows. I didn’t quite accommodate all the fudging that went on in the original. I notice that my other button is off as well. I can back out some changes, or revert and do over. Let’s revert but we’ll follow the same incremental scheme. And I think I’ll commit much more often this time. We begin:

function drawTouchToStart()
    pushMatrix()
    pushStyle()
    stroke(255)
    strokeWidth(1)
    fill(255)
    fontSize(10)
    textMode(CORNER)
    rectMode(CORNER)
    GameRunner:scaleAndTranslate()
    local s1 = "Touch for\nOne Player"
    local s2 = "Touch for \nTwo Players"
    local y = Constant:saucerY() - 4
    local sx,sy = textSize(s1)
    fill(0)
    rect(8,y-4, 16+sx, sy+8)
    sx,sy = textSize(s2)
    rect(208-sx, y-4, 16+sx, sy+8)
    fill(255)
    text("Touch for\nOne Player", 16, y)
    x = 216-textSize("Two Players")
    text("Touch for\nTwo Players", x, y)
    popStyle()
    popMatrix()
end

We extract function:

function drawTouchToStart()
    pushMatrix()
    pushStyle()
    stroke(255)
    strokeWidth(1)
    fill(255)
    fontSize(10)
    textMode(CORNER)
    rectMode(CORNER)
    GameRunner:scaleAndTranslate()
    drawButtons()
    popStyle()
    popMatrix()
end

function drawButtons()
    local s1 = "Touch for\nOne Player"
    local s2 = "Touch for \nTwo Players"
    local y = Constant:saucerY() - 4
    local sx,sy = textSize(s1)
    fill(0)
    rect(8,y-4, 16+sx, sy+8)
    sx,sy = textSize(s2)
    rect(208-sx, y-4, 16+sx, sy+8)
    fill(255)
    text("Touch for\nOne Player", 16, y)
    x = 216-textSize("Two Players")
    text("Touch for\nTwo Players", x, y)
end

We change the fill color (new idea).

function drawButtons()
    local s1 = "Touch for\nOne Player"
    local s2 = "Touch for \nTwo Players"
    local y = Constant:saucerY() - 4
    local sx,sy = textSize(s1)
    fill(0,0,0,0)
    rect(8,y-4, 16+sx, sy+8)
    sx,sy = textSize(s2)
    rect(208-sx, y-4, 16+sx, sy+8)
    fill(255)
    text("Touch for\nOne Player", 16, y)
    x = 216-textSize("Two Players")
    text("Touch for\nTwo Players", x, y)
end

This makes the fill transparent, so it’ll match the background whatever it is. That’s what I had in mind anyway.

Now use the text names properly.

function drawButtons()
    local s1 = "Touch for\nOne Player"
    local s2 = "Touch for \nTwo Players"
    local y = Constant:saucerY() - 4
    local sx,sy = textSize(s1)
    fill(0,0,0,0)
    rect(8,y-4, 16+sx, sy+8)
    sx,sy = textSize(s2)
    rect(208-sx, y-4, 16+sx, sy+8)
    fill(255)
    text(s1, 16, y)
    x = 216-textSize("Two Players")
    text(s2, x, y)
end

Now my theory was that this:

    x = 216-textSize("Two Players")

Was equivalent to using the existing sx. I think I need to check that.

function drawButtons()
    local s1 = "Touch for\nOne Player"
    local s2 = "Touch for \nTwo Players"
    local y = Constant:saucerY() - 4
    local sx,sy = textSize(s1)
    fill(0,0,0,0)
    rect(8,y-4, 16+sx, sy+8)
    sx,sy = textSize(s2)
    rect(208-sx, y-4, 16+sx, sy+8)
    fill(255)
    text(s1, 16, y)
    assert(sx == textSize("Two Players"))
    x = 216 - sx
    text(s2, x, y)
end

Works. Remove assert.

Now group the two texts separately, including additional fill setting as needed:

function drawButtons()
    local y = Constant:saucerY() - 4
    
    local s1 = "Touch for\nOne Player"
    local sx,sy = textSize(s1)
    fill(0,0,0,0)
    rect(8,y-4, 16+sx, sy+8)
    fill(255)
    text(s1, 16, y)
    
    local s2 = "Touch for \nTwo Players"
    sx,sy = textSize(s2)
    fill(0,0,0,0)
    rect(208-sx, y-4, 16+sx, sy+8)
    fill(255)
    x = 216 - sx
    text(s2, x, y)
end

Everything is good. Commit: extract and regroup drawButtons. Full disclosure: it took me three times to get the above to where I liked it.

Now extract a function leftButton. We need to be clear on whether the function specifies the box position or the text position. The choice seems to me to be arbitrary. Let’s choose the box. First I’ll extract it with just the text string as a parameter and the other values literal inside.

function drawButtons()
    local y = Constant:saucerY() - 4
    
    leftButton("Touch for\nOnePlayer")
    
    local s2 = "Touch for \nTwo Players"
    sx,sy = textSize(s2)
    fill(0,0,0,0)
    rect(208-sx, y-4, 16+sx, sy+8)
    fill(255)
    x = 216 - sx
    text(s2, x, y)
end

function leftButton(s1)
    local sx,sy = textSize(s1)
    fill(0,0,0,0)
    rect(8,y-4, 16+sx, sy+8)
    fill(255)
    text(s1, 16, y)
end

Copy paste, should work. Doesn’t, because I need the y value in there.

function drawButtons()
    local y = Constant:saucerY() - 4
    
    leftButton("Touch for\nOnePlayer", y)
    
    local s2 = "Touch for \nTwo Players"
    sx,sy = textSize(s2)
    fill(0,0,0,0)
    rect(208-sx, y-4, 16+sx, sy+8)
    fill(255)
    x = 216 - sx
    text(s2, x, y)
end

Perfect. Commit: extract leftButton function.

Let’s reduce magic numbers to be sure we know what’s going on. (Yes, I could figure this out. Let’s let the code itself participate in figuring it out.)

function leftButton(s1, y)
    local boxX = 8
    local sx,sy = textSize(s1)
    fill(0,0,0,0)
    rect(boxX,y-4, 16+sx, sy+8)
    fill(255)
    text(s1, boxX + 8, y)
end

I picked boxX to pull out because it’s clearly one of the parameters. I note in passing that the current Y parm is not the y for the rectangle. We’ll deal with that. Let’s check this. Still looks good. Let’s fix the y with a boxY trick so that we can say boxX, boxY in the rect.

function leftButton(s1, y)
    local boxX = 8
    local boxY = y - 4
    local sx,sy = textSize(s1)
    fill(0,0,0,0)
    rect(boxX,boxY, 16+sx, sy+8)
    fill(255)
    text(s1, boxX + 8, y)
end

Now let’s pass those values in from above. We’ll adjust y by 4 right now, which probably breaks the right button. No, that will confuse me. I’ll adjust it in the call for now.

    leftButton("Touch for\nOnePlayer", 8, y-4)

So therefore ...

~~~lua
function leftButton(s1, boxX, boxY)
    local y = boxY + 4
    local sx,sy = textSize(s1)
    fill(0,0,0,0)
    rect(boxX,boxY, 16+sx, sy+8)
    fill(255)
    text(s1, boxX + 8, y)
end

Rename y:

function leftButton(s1, boxX, boxY)
    local textY = boxY + 4
    local sx,sy = textSize(s1)
    fill(0,0,0,0)
    rect(boxX,boxY, 16+sx, sy+8)
    fill(255)
    text(s1, boxX + 8, textY)
end

Create textX to go with textY:

function leftButton(s1, boxX, boxY)
    textX = boxX + 8
    local textY = boxY + 4
    local sx,sy = textSize(s1)
    fill(0,0,0,0)
    rect(boxX,boxY, 16+sx, sy+8)
    fill(255)
    text(s1, textX, textY)
end

The magic numbers are interesting. The first 8 is the left-right margin. The 4 is the top-bottom margin. The second 8 is two times the top-bottom spacing. The 16 is 2 times the left-right margin. Therefore:

function leftButton(s1, boxX, boxY)
    local margin = 8
    local spacing = 4
    local textX = boxX + margin
    local textY = boxY + spacing
    local sx,sy = textSize(s1)
    fill(0,0,0,0)
    rect(boxX,boxY, 2*margin+sx,2*spacing+sy)
    fill(255)
    text(s1, textX, textY)
end

I think that’s fit for service. Commit: leftButton function is fit for service.

This has taken a while. I think I started this effort at 0930 and it i s 1030 now. But we’re quite solid. We could ship right now. I’m of a mind to pull out the other button.

Our right button wants to be given its right end, not the left end, so that we can right justify it And I think we’re right-justifying the box at 216:

function drawButtons()
    local y = Constant:saucerY() - 4
    
    leftButton("Touch for\nOnePlayer", 8, y-4)
    
    local s2 = "Touch for \nTwo Players"
    sx,sy = textSize(s2)
    fill(0,0,0,0)
    rect(208-sx, y-4, 16+sx, sy+8)
    fill(255)
    x = 216 - sx
    text(s2, x, y)
end

The 208 - sx is a bit of a red herring. It’s accommodating the known margin of 8 on one side. The thing to do here is to call the new function with the parameters I want, which (I think) are 216 and y-4. I’ll do the same as before, but this time I’ll pass in the parameters, but paste in all the existing lines as well.

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

function rightButton(s2, boxRightX, boxY)
    sx,sy = textSize(s2)
    fill(0,0,0,0)
    rect(208-sx, y-4, 16+sx, sy+8)
    fill(255)
    x = 216 - sx
    text(s2, x, y)
end

Forgot to change to use boxY for y. But the fact is, I plan to call rightButton, so let’s just compute what we need to call it. First this:

function rightButton(s2, boxRightX, boxY)
    sx,sy = textSize(s2)
    leftButton(s2, 208-sx, boxY)
end

Then that 208, which is our boxRightX - 8, our margin.

function rightButton(s2, boxRightX, boxY)
    local margin = 8
    sx,sy = textSize(s2)
    leftButton(s2, boxRightX-margin-sx, boxY)
end

This begins to make sense. And the picture is the same as it was.

Now then. My original plan with this was to reposition the buttons a bit, but time has run out, so we’ll leave the feature change for next time. We have refactored to make that change easy, though I think we may want a bit more refactoring still. We’l see. For now, commit: buttons drawn with proper functions.

Let’s Sum Up

Again today, we have proceeded rather smoothly. We started the game in attract mode, in part by adding a rather nice alert to the GameRunner to tell it whether a real Gunner or a zombie has been spawned.

Then we cleaned up the buttons a bit, moving them to the background and making their own backgrounds transparent, so they are less obtrusive and the saucer flies above them.

We began the process of adjusting their size a bit. We could have just hammered on the values in the original button drawing code, and we’d probably be done now. But that code was quite ad hoc and hard to understand, so instead we refactored it. And we did so quite smoothly. There was one revert in there, which was probably due to a typo, but I don’t know and don’t care, because after the revert things went fine. As they usually do.

The process of the refactoring was in tiny tiny steps. Each one made sense–at least to me–and the next one was guided by what I saw in the code resulting from the previous one. Each step was almost provably correct, except that instead of a refactoring tool I have a human, who is provably not provably correct, so I did a lot of looking at the screen.

I’d like to have tests for this sort of thing, so that I could just run them and have them check that things look good, but I don’t know how to write a “looks good” test.

Be that as it may, we now have a smooth landing pad for the button adjustment if we decide to do it.

And I have a sticky note with some things cross off and:

  • Make saucer less frequent
  • Saucer sound doubles
  • Adjust button size for less overlap

A good morning. See you next time!

Invaders.zip