It’s time to start on the two-player option. I have ideas but not precisely a plan.

Maybe it’s a sketch of a plan, but I think it’s just an idea. One way of thinking of a two-player game is as two games, shuffled together. If first you played a full game, and then I played a full game, we would each get our own instance of GameRunner, because of this function in Main:

function startGame(numberOfLives)
    Runner = GameRunner(numberOfLives)
    invaderNumber = 1
end

So, in principle, if we had two GameRunners, we should be able to have them take turns, one Player instance at a time. If GameRunner holds on to all the player-specific information, such as shields and score, much of the two-player behavior ought to just drop right out.

I am sometimes an optimist, but I am always a computer programmer, so I don’t think it will be quite that simple, but it seems to me that it’s probably close to workable. We’ll try it.

I did some thinking on how it ought to work. I’m not sure about the original game, but I think that once it started, there was a pause between players, without requiring a button to be pressed to signify that the second player was ready. Since for our game, the iPad or phone would have to be passed back and forth, I think we should require touch to start on each switch of turns.

It might be easiest to change the game so that touch to start is used before rezzing any Gunner. I plan to leave that option open but to require that there’s a touch to start before each player switch, if not before each gunner rezzing.

A trick

It is tempting to do stories in the order their capabilities will be applied in actual use. A common example is that teams will often implement “Log In” as their first story. I commonly argue that this is not a good choice of story, that no one really wants to log in, and that the first story should be something useful that the system does, such as “Dispense $200”. This is little more than a trick to get a team focused on stories of value, and on learning about the real issues of the product, but I think it’s a good trick.

We’ll use that trick here, with only a bit of a twist: we’ll convert our game to be a two-player game, always. Then, later, we’ll put in a button or something to set it back to one player. If we start implementing two-player right now, we’ll discover all the places where we need to move things around to ensure privacy between players.

Getting Started

At first I thought of a table of GameRunners, two of them, and that we’d loop over that table with some kind of nextRunner function. But I really don’t think we’re going to do more than two, so it is tempting to have two variables. My thought when I started this paragraph was that we’d just have something like:

    activeRunner,otherRunner = otherRunner,activeRunner

That would just swap the runners. My thought half-way through the above paragraph was, no, the table solution is better. But my thought now is that swapping is better, because it involves less code to make it work. If we want a table later we can do a table later.

Yes, I really did change my mind twice right there in front of you.

Let’s look at how Main works now:

function setup()
    runTests()
    parameter.boolean("Cheat", false)
    Runner = nil
    startGame(0)
end

function startGame(numberOfLives)
    Runner = GameRunner(numberOfLives)
    invaderNumber = 1
end

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

We start with no Runner. We use that to check whether the touch is for Main, signifying start the game, or for the Player instance, to operate him.

Now the first thing I notice is that we’re doing a two player game but the code calls the cannon gunner thing Player. I’m concerned that that will be confusing, but I’m not going to do anything about it now. Let’s see whether the name gets in our way.

I also notice that the Player instance, that is, the gunner, is a singleton. there’s really only one during the whole game, changing state from drawn, to exploding, to nothing, to zombie. That makes me wonder whether that will work even with two players.

Darn, it didn’t take long for that to get confusing, did it? Changing the name to Gunner will change some tests, many of which do this kind of thing:

            local Gunner = Player:instance()

That’s generally followed by lots of references to Gunner, so I can’t quickly change the name of the Player class to Gunner. I could rename it Cannon. But no. I’m used to calling it Gunner, so we’ll bite the bullet and rename the class.

This is a refactoring, changing no function (if we do it right) that is being done to give us an easier implementation of our two-player game, by making the name space less confusing.

Here goes. I let Codea’s somewhat flaky replace do most of the work for me, but I copied the class itself over to Sublime and edited it there and pasted it back. I’ve still not done the tests and I think I’ll do those the same way. First, I just have to try it to see what explodes.

The tests crash so badly that I can’t run the game. That’s surely for the best. I’ll take the tests over to Sublime and clean them up.

This has gone fairly well, but as usual Codea’s replace hasn’t quite done the trick. Two tests are failing.

Gunner:82: attempt to call a nil value (method 'playerExploding')
stack traceback:
	Gunner:82: in method 'explode'
	Bomb:80: in method 'killsGunner'
	Bomb:44: in method 'checkCollisions'
	Bomb:40: in method 'update'
	Army:154: in method 'update'
	GameRunner:88: in method 'update60ths'
	GameRunner:76: in method 'update'
	Main:38: in function 'draw'

That’s this code:

function Gunner:explode()
    if Cheat then return end
    Runner:playerExploding()
    self.alive = false
    self.count = 240
    self.drawingStrategy = self.drawExplosion
    Runner:soundPlayer():play("explosion")
end

I suspect that we got a rename wrong, but I was sure that I fixed it:

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

Has something not been saved? Running again, new message:

32: Saucer Score depends on shots fired -- Saucer:101: attempt to index a nil value (global 'Player')
function Saucer:score()
    return self.scores[Player:instance():shotsFired()%15 + 1]
end

Missed a rename. A refactoring tool would come in handy right about now.

Changing that makes the tests and the game run fine. Let’s commit: Rename Player class to Gunner.

It’s 0809: I got up early today. I hope you’ll forgive me while I shave and head out for chai. BRB …

See, that didn’t take long at all, did it? I did have a thought somewhere along the way, and it goes like this:

You don’t seem to design a lot, Ron

Now I’d argue that I’m designing all the time, but if we’re thinking about things like figuring out just how we’re going to turn a game that uses one GameRunner to one that uses two, with all the odd handoff stuff that will surely come up–well, no, I don’t worry about that much. I’m most often comfortable taking a first step, then discovering what the next step needs to be. That might seem like a dangerous thing to do. I claim that it isn’t.

We have separation of concerns

First, I know that the program is divided into objects and methods that isolate various conceptual notions into separate sections of code. Those sections do interact, but they interact, by and large, in fairly simple and consistent ways. This means that my changes will mostly divide into two types, changing the insides of an object, and, less often, changing how other objects talk to it.

Now one might think that’s all very well and good in a tiny little program like this one, but if this program were ten or 100 times as large as it is, I’d still build it out of objects that were no more than a couple of hundred lines of code, made of functions that probably average less than ten lines each. So even in a large program, my changes would mostly be isolated.

There would be occasions where a change to the protocol of some object required changes “all over” … but that fact, when it occurs, tells us that we have an idea–whatever that code is about–that isn’t sufficiently isolated. Quite likely, we’d isolate it and reduce the number of changes needed.

And there are other techniques, such as leaving an old interface alive for a while, allowing an incremental switch-over to the new one, and so on.

We have a code manager

Sometimes I do get into trouble with my first attempt to just step forward bit by bit. It usually takes me longer than it should to notice that I’m in trouble, but when I do, I can revert the code to a recent clean point and start over on my new change. At that point, I’ll have learned a lot, and probably have a much better idea how to do the thing. I rarely ever have to revert twice. And, of course, reverting is a point at which one takes a moment to think about the whole path, looking forward a bit more, because now we know it’s needed.

And therefore …

Yes, I do tend to start at one end of the path and just move along it until I’m done, and yes, I do recommend that you think about that approach, and maybe try it for yourself.

Just don’t forget the part about revert. You’ll need it sometimes, and sometimes you’ll need it badly.

Two GameRunners

With that in mind, let’s put in two GameRunners and see if we can make them cooperate. Remember the Main tab:

function setup()
    runTests()
    parameter.boolean("Cheat", false)
    Runner = nil
    startGame(0)
end

function startGame(numberOfLives)
    Runner = GameRunner(numberOfLives)
    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(3)
        end
    end
end

Let’s create our two GameRunners right in setup, and then when the screen is touched, deal with the swapping. I’m not quite sure how to manage that, because of the check for Runner being nil. I’m going to pretend that I’m not worried about that:

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

I removed the startGame() call, and I decided that the GameRunner will get a new text parameter, the player number. Text because I figure we’ll display it. It should really be the first parameter, but I don’t want to change that much code right now.

Now in touch

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

We’ll call startGame but not pass in the number of lives because we’ve already created our GameRunner Players. (Do you think we may rename GameRunner to Player? I suppose we might, someday.) Anyway, startGame looks like this:

function startGame(numberOfLives)
    Runner = GameRunner(numberOfLives)
    invaderNumber = 1
end

Clearly we have to remove the runner creation. But what if we toss a player into Runner and then swap the players?

function startGame(numberOfLives)
    Runner = Player1
    Player1,Player2 = Player2, Player1
    invaderNumber = 1
end

I’m pretty sure that this will do something interesting. I’m not clear at this instant why the zombie game plays but anyway I’m going to run this and see what happens.

Main:90: attempt to index a nil value (global 'Runner')
stack traceback:
	Main:90: in function 'showTests'
	Main:33: in function 'draw'
function showTests()
    if not CodeaUnit then return end
    if not CodeaVisible then return end
    pushMatrix()
    pushStyle()
    fontSize(50)
    textAlign(CENTER)
    if not Console:find("0 Failed") then
        stroke(255,0,0)
        fill(255,0,0)
    elseif not Console:find("0 Ignored") then
        stroke(255,255,0)
        fill(255,255,0)
    else
        fill(0,128,0)
    end
    local sc = Runner:scaleAndTranslationValues(WIDTH,HEIGHT)
    local y = (Constant:saucerY() + 0x20)*sc
    text(Console, WIDTH/2, y)
    popStyle()
    popMatrix()
end

The bug is that call to scaleAndTranslate. I think that code requires no member accesses, let’s look:

function GameRunner:scaleAndTranslationValues(W,H)
    local gameScaleX = 224
    local gameScaleY = 256
    rectMode(CORNER)
    spriteMode(CORNER)
    local sc = math.min(W/gameScaleX, H/gameScaleY)
    local tr = W/(2*sc)-gameScaleX/2
    return sc,tr
end

Great. We can call it on the class:

    local sc = GameRunner:scaleAndTranslationValues(WIDTH,HEIGHT)

Probably this function should reside somewhere else. Maybe in Constants, even though it isn’t necessarily constant. Run again, see what explodes.

Main:48: attempt to index a nil value (global 'Runner')
stack traceback:
	Main:48: in function 'drawTouchToStart'
	Main:36: in function 'draw'

You’d think I could at least look to see what’s going on. The habit of not looking probably traces back to my Smalltalk days, where we’d often code in the debugger. It would walk back on some missing method, we’d code it in place and proceed. Yes, proceed after adding a method.

function drawTouchToStart()
    pushStyle()
    stroke(255)
    fill(255)
    fontSize(50)
    local sc = Runner:scaleAndTranslationValues(WIDTH,HEIGHT)
    local y = (Constant:saucerY() + 0x10)*sc
    text("Touch to Start", WIDTH/2, y)
end

Same issue, same fix.

Now when I run, I get a blank screen saying Touch to Start. I’m gonna touch it.

The game runs, and when it ends, it shows Game Over and begins zombie play. It’s definitely not swapping players on each gunner death. We don’t expect that it would. Let’s do something about seeing which player is running:

function GameRunner:init(numberOfLives, playerName)
    self.lm = LifeManager(numberOfLives or 3, self.spawn, self.gameOver)
    self.playerName = playerName or "void"
    Shield:createShields()
    Gunner:newInstance()
    TheArmy = Army()
    self.sounder = Sound()
    self.line = image(208,1)
    for x = 1,208 do
        self.line:set(x,1,255, 255, 255)
    end
    self:resetTimeToUpdate()
    self.weaponsTime = 0
end

I decided to handle the case of someone not providing a name, just in case.

Now to display it somewhere.

How about here:

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

We can add it to the score line, though we’d better realign it as well.

    text("Player "..self.playerName.." SCORE " .. tostring(TheArmy:getScore()), 125, 4)

That gives us this:

player score

So that’s nice. I wonder what would happen if after the Gunner explodes, we called our startGame function. I also wonder just what happens now when a gunner is killed.

function Gunner:explode()
    if Cheat then return end
    Runner:playerExploding()
    self.alive = false
    self.count = 240
    self.drawingStrategy = self.drawExplosion
    Runner:soundPlayer():play("explosion")
end

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

That’s a bit enlightening but not terribly so. There’s also this:

function Gunner:update()
    self.missile:update()
    self:manageMotion()
    self:manageCount()
end

And this:

function Gunner:manageCount()
    if self.count <= 0 then return end
    self.count = self.count - 1
    self:manageExplosion()
    self:manageSpawning()
end

function Gunner:manageSpawning()
    if self.count <= 0 then
        Runner:requestSpawn()
    end
end

This is where we get a new gunner:

function GameRunner:requestSpawn()
    self.lm:next()(self)
end

function LifeManager:next()
    self.life = math.min(self.life+1, #self.lives)
    return self:current()
end

function LifeManager:current()
    return self.lives[self.life]
end

We recall that LifeManager has a table of lives that it uses one at a time:

function LifeManager:init(count, live, dead)
    self.lives = {}
    for i = 1,count do
        table.insert(self.lives,live)
    end
    table.insert(self.lives,dead)
    self.life = 0
end

And those are initialized to be functions:

function GameRunner:init(numberOfLives, playerName)
    self.lm = LifeManager(numberOfLives or 3, self.spawn, self.gameOver)

So when we do this:

function GameRunner:requestSpawn()
    self.lm:next()(self)
end

We expect an immediate call back either to spawn or gameOver:

function GameRunner:gameOver()
    self:spawn(true)
end

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

That’s a bit intricate, isn’t it? Be that as it may, we need to take control after the gunner is done exploding. Which reminds me, that name should be changed to gunnerExploding. I hesitate to do it now in the middle of this experiment. Remind me later, OK?

I think we should hit it right here with our hammer:

function Gunner:manageSpawning()
    if self.count <= 0 then
        Runner:requestSpawn()
    end
end

We now want to inform the GameRunner that the current Gunner is fully dead now, having accomplished all its quaint and curious death throes. We’ll leave it up to the GameRunner to decide what to do next. Therefore:

function Gunner:manageSpawning()
    if self.count <= 0 then
        Runner:gunnerDead()
    end
end

And

function GameRunner:gunnerDead()
    
end

function GameRunner:requestSpawn()
    self.lm:next()(self)
end

I was briefly tempted here to do the switch and then request the spawn. But I’m in one of the instances. I think we should call up to Main to do this. And I’m going to let it do the request right after a swap. So, for now, a new function in Main:

function GameRunner:gunnerDead()
    nextPlayersTurn()
end
function nextPlayersTurn()
    Runner = Player1
    Player1,Player2 = Player2, Player1
    invaderNumber = 1
    Runner:requestSpawn()
end

This will do something interesting. I’m not entirely sure what.

Amazingly, it nearly works. I’ll make a video.

two players

You might notice that it starts out saying Player 1 but as soon as the first gunner spawns, it’s saying Player 2. But after that it is switching between players. So that’ll be an init problem, probably the initial swap shouldn’t happen. We also notice that the two players are getting the same shields. What should happen is that each player gets their own.

I bet we can fix that fairly readily …

function GameRunner:init(numberOfLives, playerName)
    self.lm = LifeManager(numberOfLives or 3, self.spawn, self.gameOver)
    self.playerName = playerName or "void"
    Shield:createShields()
    Gunner:newInstance()

Well it looks like shields is one of those cool singletons I was so in love with:

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

Thereafter, the Shield class refers to that (class) variable Shield.shields for drawing and such.

What if we were to save that value in GameRunner and restore it?

function GameRunner:init(numberOfLives, playerName)
    self.lm = LifeManager(numberOfLives or 3, self.spawn, self.gameOver)
    self.playerName = playerName or "void"
    Shield:createShields()
    self.shields = Shield.shields.
    Gunner:newInstance()
    TheArmy = Army()
    self.sounder = Sound()
    self.line = image(208,1)
    for x = 1,208 do
        self.line:set(x,1,255, 255, 255)
    end
    self:resetTimeToUpdate()
    self.weaponsTime = 0
end
function GameRunner:requestSpawn()
    Shield.shields = self.shields
    self.lm:next()(self)
end

This is definitely a hack and needs to be made right. But will it work?

GameRunner:12: attempt to index a nil value (field 'Gunner')
stack traceback:
	GameRunner:12: in field 'init'
	... false
    end

    setmetatable(c, mt)
    return c
end:24: in global 'GameRunner'
	Tests:9: in field '_before'
	CodeaUnit:44: in method 'test'
	Tests:19: in local 'allTests'
	CodeaUnit:16: in method 'describe'
	Tests:6: in function 'testCodeaUnitFunctionality'
	[string "testCodeaUnitFunctionality()"]:1: in main chunk
	CodeaUnit:139: in field 'execute'
	Main:77: in function 'runTests'
	Main:5: in function 'setup'

Hm, “the changes I made shouldn’t affect that”.1

Oh! Wow. Notice this:

    Shield:createShields()
    self.shields = Shield.shields.

There’s a dot after shields. I have no idea what that does, nor why it even ran. Anyway, without that, let’s see what happens.

shields saved

Sure enough, the players get their own shields back. Sweet.

However, the players don’t get their own invaders: they get the leftover rack from the previous player. That won’t do, will it.

But we should think about whether it is time to commit this code. It’s good progress and the tests are green. However, the game isn’t quite playable now in single player mode. We really shouldn’t release a broken game, so our options include:

  • treat the work so far as an unfinished open branch and commit it.
  • treat it as a spike from which we’ve learned a lot and revert it
  • put in a feature toggle and turn off this capability so that we can release

I do want to lock these changes in, so I’m going to commit and treat it as an open branch. If need be, I won’t release a version today. Commit: two players with own shields.

I have a little time before lunch. It’ll be a long morning but I don’t feel tired, too tense, nor too stupid yet. What about those invaders? Where are they stored?

function GameRunner:init(numberOfLives, playerName)
    self.lm = LifeManager(numberOfLives or 3, self.spawn, self.gameOver)
    self.playerName = playerName or "void"
    Shield:createShields()
    self.shields = Shield.shields
    Gunner:newInstance()
    TheArmy = Army()
    self.sounder = Sound()
    self.line = image(208,1)
    for x = 1,208 do
        self.line:set(x,1,255, 255, 255)
    end
    self:resetTimeToUpdate()
    self.weaponsTime = 0
end

It’s that global. We’ve actually thrown away one army and created another when we built our two GameRunners. We could do the same trick and cache them … but let’s see who’s referencing The Army. Maybe there’s something better to do.

GameRunner itself refers to it many times. That’s no problem, we can easily fetch it from a member variable. There are other references, but since Runner is already global, can’t we just change everyone? We’ve got a clean slate due to the commit we just did. Let’s see.

This looks odd:

function Army:addToScore(added)
    self.score = self.score + added
    TheArmy:adjustTiming(self.score)
end

We’re in an instance of Army. Why can’t we say self there? Let’s do. We’ll see what happens.

function Sound:updateToneInterval()
    if self.toneNumber ~= 3 then return end
    self.toneInterval = self.toneCounts[self:invaderIndex(TheArmy.armySize)]/60
end

This we change:

function Sound:updateToneInterval()
    if self.toneNumber ~= 3 then return end
    self.toneInterval = self.toneCounts[self:invaderIndex(Runner.army.armySize)]/60
end

I now expect each player to get their own army. And I expect no errors. That expectation rarely pans out..

two armies

That actually seems to work. I think we have both shields and army nicely packaged up in GameRunner now.

Commit: two players have own army.

Summing Up

Let’s call it a morning and commit to a Wendy’s spicy chicken.

Summing up, this has gone as nicely as one might hope. We’re not done, but we have two players taking turns, although #2 goes first, and they have their own shields and their own armies of invaders to fight. We’ve got code where everything we set out to do works, but some of it is a bit iffy, notably the smushing of the Shield table. We can foresee just saving the shields in GameRunner and using them as we did with the Army, so that should be easily made right.

We can’t really make a new release right now. The feature, as defined, requires more than one session’s work, so we have to leave at least a feature branch open. Since it’s just me working here, I’ve committed to main, but the principle is the same.

I think there are two lessons waiting to be drawn here, though it is a bit premature to declare victory.

First, having a pretty good design of objects and instances makes a rather significant global change pretty straightforward. We’ll want to talk about whether some of that fancy stuff with singletons was a red herring, but I’m inclined to think right now that it was.

Second, the process of making a small change and seeing what needs to be done next, without a lot of grand designing, has worked well. That’s not enough to say we should all start diving into murky water without looking, but it is enough to suggest that there are reasonable cases where it’s a decent thing to try.

Let’s see what happens next time. See you then!

  1. Programmer statement commonly made after their changes did affect that.