Space Invaders 71
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:
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.
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.
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..
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!
-
Programmer statement commonly made after their changes did affect that. ↩