Space Invaders 14
It’s 6 bloody AM and I’m programming. What’s up with that?
Well, I woke up and my brain started running. So here we are. I sort of have a plan.
Our fundamental mission is to move forward smoothly from the trivial not-quite-a-game thing with the square invaders, to an actual playable game. I expect this to be interesting, because it’s already quite a mess, as we’ll discuss shortly.
In the early spikes for the invaders, I imported bitmaps for them, and did some experiments for moving them. Recall that the original game moved only one invader each sixtieth of a second, and that we’re going to replicate that behavior even though in Codea we actually draw the whole screen in every cycle. Moving one invader per sixtieth of a second gives their motion a nice rippling effect, and anyway, I just wanted to do it.
The original game moved faster and faster, primarily because as you shoot down the invaders, there are fewer and fewer to move. When there are 55 invaders on the screen, it takes almost a full second to move them one step, and it’s quite a few steps (details later) before they take another step downward in that inexorable threatening manner that they have.
When there are only a few invaders, say six, it would only take one tenth of a second to move them all, so they progress more rapidly. When there’s just one, she really whips around the screen. (She also takes bigger steps going one way than going the other.)
So our mission is to replicate that behavior to the extent that we can. And, as I mentioned, I have a plan.
What shall we do today, Brain? The same thing we do every day, Pinky, try to create a world.
I propose to have a class, called Army
, that will be a container for members of another class called Invader
. When it is time to draw, Army
will draw all the Invaders
. When it is time to update (every sixtieth of a second), Army will know the index of the invader whose turn it is to be updated. It will tell that invader to update. If the invader is present, it will update and return false
. If it is dead, the invader will return true
. Army
will then call update on the next invader, and so on.
There’s a reason why it should be done that way:
Tell, don’t ask.
One of the fundamental ideas in object-oriented programming is that we don’t ask objects questions, we tell them to do things. With a procedural approach, Army might check the invader to see if it was alive, and skip forward until it finds one that needs updating. That’s “ask, don’t tell”, and it’s generally not the best design.
So we’ll tell, not ask. And we’ll see how it turns out.
So that’s the overall plan for the invaders, and despite that I thought about it at 5 AM, I’m feeling pretty good about it.
The next question is how to get there, and for that we’ll have to look at our code.
Reviewing the Current Code
All our work is being done now in one tab, Main, and it’s pretty procedural, but it does have a bit of a decent breakout due to a lot of it having been programmed by expressing our intention. So we have setup:
function setup()
runTests()
setupInvaders()
setupGunner()
end
Seems OK. We have draw:
function draw()
pushMatrix()
pushStyle()
background(40, 40, 50)
showTests()
stroke(255)
fill(255)
scale(4) -- makes the screen 1366/4 x 1024/4
translate((1366-1024)/8,0)
drawInvaders()
drawGunner()
drawMissile()
popStyle()
popMatrix()
end
By the way, the runTests
, showTests
does the automatic running of the CodeaUnit tests. None of those are very interesting, they just calculate and record some screen values and such. Maybe we’ll write more of them. I hope we do, but it’s often hard for me to think of something to test that isn’t easier to just run and watch.
Since we’re working on the invaders this morning, let’s look at their setup and drawing code as it stands now:
function setupInvaders()
Invaders = {}
for row = 1,5 do
for col = 1,11 do
local p = vec2(col*16, 256-row*16)
table.insert(Invaders, {pos=p, alive=true})
end
end
Missile = {v=0, p=vec2(0,0)}
end
function drawInvaders()
pushMatrix()
pushStyle()
rectMode(CENTER)
for i,invader in ipairs(Invaders) do
if invader.alive then rect(invader.pos.x, invader.pos.y, 8,8) end
end
popStyle()
popMatrix()
end
We want to evolve this program in tiny steps, keeping it working, and making it work better, all the time. And we know we’re going to move to an Army
class and and Invader
class.
Well, I say “know”. That’s the current plan. I feel it’s a pretty good one, but there’s great value in holding on the the ideas but with a light enough commitment that we remain open to discovering better ways to do things. We’ll succeed if we get a well-structured program that plays the game. We don’t get extra credit for sticking with a plan if that plan can be improved.
For now, though, I want to make some progress. Should I start by pulling out Army
, or Invader
? I feel that the invader class will be more interesting and capable, so let’s start with that. (I started to say more here about why I think we should start with Invader
but other than a sense that it’ll be a place for good stuff to be done, I got nuttin. Seems like a good idea, let’s try it.)
I ask Codea to create a new class, Invader
, and it gives me this:
Invader = class()
function Invader:init(x)
-- you can accept and set parameters here
self.x = x
end
function Invader:draw()
-- Codea does not automatically call this method
end
function Invader:touched(touch)
-- Codea does not automatically call this method
end
I hate that template, but as far as I know there’s no way to change it. So, manually:
Invader = class()
function Invader:init()
end
function Invader:draw()
end
Not very useful yet, but we do have a function to set up invaders, so let’s see if we can use it:
function setupInvaders()
Invaders = {}
for row = 1,5 do
for col = 1,11 do
local p = vec2(col*16, 256-row*16)
table.insert(Invaders, {pos=p, alive=true})
end
end
Missile = {v=0, p=vec2(0,0)}
end
This code creates a global table Invaders
. That’ll be about where our Army
class stands, someday. Into that table it inserts a small table for each invader, containing a position and an alive flag. So let’s instead create an Invader. We’ll give it the position, but it can deal with the alive flag itself:
function setupInvaders()
Invaders = {}
for row = 1,5 do
for col = 1,11 do
local p = vec2(col*16, 256-row*16)
table.insert(Invaders, Invader(p)) -- <---
end
end
Missile = {v=0, p=vec2(0,0)}
end
That requires us to init the invader:
function Invader:init(pos)
self.pos = pos
self.alive = true
end
I used those same member names because, right now, the rest of the code is asking the table entries for those values, and doing stuff.
I think that the program should still run, although I am a bit reticent about it. Let’s see.
In fact it does run and I can even shoot down the stationary invaders. Let’s see about moving some behavior into the Invader class, starting with draw:
Oh wait, commit: Invader class initial version.
function drawInvaders()
pushMatrix()
pushStyle()
rectMode(CENTER)
for i,invader in ipairs(Invaders) do
if invader.alive then rect(invader.pos.x, invader.pos.y, 8,8) end
end
popStyle()
popMatrix()
end
In the spirit of “tell, don’t ask”, we’ll just, well, tell the invaders to draw:
function drawInvaders()
pushMatrix()
pushStyle()
rectMode(CENTER)
for i,invader in ipairs(Invaders) do
invader:draw()
end
popStyle()
popMatrix()
end
That requires that the invaders know how to draw themselves:
function Invader:draw()
if self.alive then
rect(self.pos.x, self.pos.y, 8,8)
end
end
Game still runs. Commit: invaders draw selves.
What else? The Main tab is still checking for collisions and telling invaders not to be alive. There’s a lot of code for that:
function drawMissile()
if Missile.v == 0 then return end
rect(Missile.pos.x, Missile.pos.y, 2,4)
Missile.pos = Missile.pos + vec2(0,0.5)
checkForKill(Missile)
end
function checkForKill(missile)
for i, invader in ipairs(Invaders) do
if isHit(missile,invader) then
invader.alive = false
missile.v = 0
end
end
end
function isHit(missile, invader)
if not invader.alive then return false end
local missileTop = missile.pos.y + 4
local invaderBottom = invader.pos.y - 4
if missileTop < invaderBottom then return false end
local missileLeft = missile.pos.x-1
local invaderRight = invader.pos.x + 4
if missileLeft > invaderRight then return false end
local missileRight = missile.pos.x + 1
local invaderLeft = invader.pos.x - 4
return missileRight > invaderLeft
end
I’m tempted to “figure this out” and decide what goes where. And I’m capable of doing that, even at 0650 on a Wednesday. But let’s just follow our nose a bit. We can certainly move isHit
to Invader. Let’s just do that and see what we get.
function checkForKill(missile)
for i, invader in ipairs(Invaders) do
if invader:isHit(missile) then -- <---
invader.alive = false
missile.v = 0
end
end
end
I was going to delete the setting of alive
there, but then I thought better of it. Doing that requires me to remember two things, moving isHit
and dealing with the flag. One thing at a time is better.
function Invader:isHit(missile)
if not self.alive then return false end
local missileTop = missile.pos.y + 4
local invaderBottom = self.pos.y - 4
if missileTop < invaderBottom then return false end
local missileLeft = missile.pos.x-1
local invaderRight = self.pos.x + 4
if missileLeft > invaderRight then return false end
local missileRight = missile.pos.x + 1
local invaderLeft = self.pos.x - 4
return missileRight > invaderLeft
end
The game still works. Commit: invader checks hit.
Now let’s go back and fix that invasive flag-setting:
function checkForKill(missile)
for i, invader in ipairs(Invaders) do
if invader:isHit(missile) then
invader.alive = false
missile.v = 0
end
end
end
No reason to set the alive flag from here, we should do it inside the invader. But the invader’s isHit
method has no good spot for setting the flag, so instead we’ll posit a new method:
function checkForKill(missile)
for i, invader in ipairs(Invaders) do
if invader:killedBy(missile) then
missile.v = 0
end
end
end
And we can do killedBy
like this:
function Invader:killedBy(missile)
local hit = self:isHit(missile)
if hit then self.alive = false end
return hit
end
Works fine. Commit: Invader handles kills internally.
Now I don’t really like that killedBy
method and I have a clever idea. It’s a small one, so I think I’ll let it live:
function Invader:killedBy(missile)
self.alive = not self:isHit(missile)
return not self.alive
end
Is that too clever? It winds up with two not
s in it. Maybe the flag should be dead
. But I think it’s too soon to decide on that. Commit: refactor killedBy.
Let’s see if there’s any other code messing about with the innards of the invaders. It appears to me that there is not. I don’t have any great tools for checking that but there are no occurrences of invader
in Main, so that’s a decent sign.
What now? We could begin moving the invaders. That would give our next release a bit more capability than yesterday’s. I’m inclined to lift the code from the old spike. I’m certainly going to at least look at it.
Ow. I nearly regret that. Here’s the update code from the spike:
function Rank:update(elapsed)
if elapsed < self.lastElapsedTime + self.timeToUpdate then return end
if #self.undone ~= 0 then
local invader = self.undone[1]
table.remove(self.undone, 1) -- costly
invader.p =invader.p + self.updateStep
else -- all moved
self.imageToggle = self.imageToggle == 2 and 1 or self.imageToggle + 1
self:copyToUndone()
self.updateStep.y = 0
if self.updateStep.x > 0 and self.invaders[#self.invaders].p.x > 224 - 16 then
self.reverse = true
elseif self.updateStep.x < 0 and self.invaders[1].p.x < 0 then
self.reverse = true
end
if self.reverse then
self.updateStep = - self.updateStep - vec2(0,2)
self.reverse = false
end
end
self.lastElapsedTime = elapsed
end
This code maintains a separate list of invaders who aren’t moved, moves one, and updates the list. I am not at all sure what tiny fool wrote this code but it surely could have been done better. And we do have a plan for that in mind.
I think with that mess very loosely in mind, we’d best write a new update, by intention, something like this:
function updateInvaders()
if itIsTimeToUpdate() then
resetTimeToUpdate()
local continue = true
while(continue) do
continue = nextInvader():update(motion)
end
end
end
There are a lot of assumptions about initialization and such in that code but the basic idea is that if it’s time to update, we’ll reset the update clock and then update invaders until one comes back saying not to continue. Recall that the idea is that if they’re not alive they’ll say to continue, otherwise they won’t, and we’ll therefore just do one.
Let’s at least get them moving, by initializing up in setup and see if we can make this happen. I’m hoping it isn’t too big a bite.
function setup()
runTests()
setupInvaders()
setupGunner()
motion = vec2(2,0)
invaderNumber = 1
end
And I’ve commented out some of the update:
function updateInvaders()
-- if itIsTimeToUpdate() then
-- resetTimeToUpdate()
local continue = true
while(continue) do
continue = nextInvader():update(motion)
end
-- end
end
Now “next invader` is just this, sort of:
function nextInvader()
if invaderNumber > #Invaders then
invaderNumber = 1
end
local inv = Invaders[invaderNumber]
invaderNumber = invaderNumber + 1
return inv
end
I’m feeling pretty nervous about this. It’s getting a bit complicated, and my wife is chatting with me about yesterday’s primary election. If this doesn’t come together quickly, I’ll revert.
Whew, it does what I expected. The invaders march to the right in a nice rippling fashion:
They will, of course, march right off the screen, because they don’t know when to turn around. But I’m going to commit now: invaders march to right.
But I need to think about the overall marching bit.
Marching Orders
The marching has some odd bits to it. Let’s see if we can tease out some structure.
- They march in some direction, usually just right or left but sometimes they step downward and right or left, if they’re reversing.
- They should reverse if a live invader has moved to the far right or left edge of the screen.
- When they reverse, each invader’s next step will include a downward component.
- After all invaders have stepped downward, the steps revert to sideways until the edge is hit again.
- All this happens one invader at a time.
I’ve already written code for this that seemed to work, but it was messy, as we saw above. Thinking about the problem, I start thinking about some kind of state machine, or a pluggable movement strategy (and perhaps pluggable edge strategy).
It seems to me that, except for the very first initialization, the decision as to what to do next other than just execute motion happens … let me rethink and rephrase that …
The decision about what kind of motion to apply happens “between the last invader moving and the first invader moving”.
Or … “before the first invader moves”.
What do those decisions look like?
At the beginning of an update cycle …
- If we hit an edge, motion should reverse and go down and sideways.
- If we’re moving down, remove the downward component and just go sideways.
Is that all there is? At least for now, that could be it. Can I just code that up, nothing too fancy?
Well, one issue is that these decisions are at the level of the whole invader army, not the individual invader. So that makes me want to refactor to an Army
class before adding this capability.
Make the code work, make it right, then move on. It (sort of) works, but it’s not righteous code. We’ll refactor.
New class: Army
:
Army = class()
function Army:init()
end
function Army:draw()
end
We’ll move functionality over to it bit by bit. We have this in Main:
function setupInvaders()
Invaders = {}
for row = 1,5 do
for col = 1,11 do
local p = vec2(col*16, 256-row*16)
table.insert(Invaders, Invader(p))
end
end
Missile = {v=0, p=vec2(0,0)}
end
Obviously that creation of Missile doesn’t belong there. But the rest does. So, in Main:
function setup()
runTests()
Invaders = Army()
setupGunner()
Missile = {v=0, p=vec2(0,0)}
motion = vec2(2,0)
invaderNumber = 1
end
I figure that Main will at least have to tell the army to draw and such, so for now, I’ll store it in the Invaders
global. When we create the army, we’ll go ahead and create the invaders:
function Army:init()
self.invaders = {}
for row = 1,5 do
for col = 1,11 do
local p = vec2(col*16, 256-row*16)
table.insert(self.invaders, Invader(p))
end
end
end
Now if I had decent tests here, I’d just run them and the failures would tell me what to fix. As it stands, I’ll run the program and do the same.
Main:35: attempt to index a nil value
stack traceback:
Main:35: in function 'updateInvaders'
Main:27: in function 'draw'
This of course means that main needs to tell the army to update. The actual failure has to do with trying to fetch an invader out of the Invaders
global, which is no longer a collection.
We’ll just push things bit by bit:
function Army:update()
-- if itIsTimeToUpdate() then
-- resetTimeToUpdate()
local continue = true
while(continue) do
continue = self.nextInvader():update(motion)
end
-- end
end
And nextInvader
:
function Army:nextInvader()
if self.invaderNumber > #self.invaders then
self.invaderNumber = 1
end
local inv = self.invaders[invaderNumber]
self.invaderNumber = self.invaderNumber + 1
return inv
end
I moved the invader number in in this code and we’ll need to initialize it. Right now that’s in initial setup, so:
function Army:init()
self.invaders = {}
for row = 1,5 do
for col = 1,11 do
local p = vec2(col*16, 256-row*16)
table.insert(self.invaders, Invader(p))
end
end
self.invaderNumber = 1
end
Let’s see what explodes. I suspect that our next nearly good state will have no crashes but also no invaders drawn, because we aren’t drawing them.
Oh heck, having thought of it I might as well do it In Main
function drawInvaders()
pushMatrix()
pushStyle()
rectMode(CENTER)
for i,invader in ipairs(Invaders) do
invader:draw()
end
popStyle()
popMatrix()
end
We’ll move that out and have Main do Invaders:draw().
function Army:draw()
pushMatrix()
pushStyle()
rectMode(CENTER)
for i,invader in ipairs(self.invaders) do
invader:draw()
end
popStyle()
popMatrix()
end
Now let’s see what blows up.
Army:28: attempt to index a nil value (local 'self')
stack traceback:
Army:28: in field 'nextInvader'
Army:22: in method 'update'
Main:27: in function 'draw'
The word “field” is the hint:
function Army:update()
-- if itIsTimeToUpdate() then
-- resetTimeToUpdate()
local continue = true
while(continue) do
continue = self.nextInvader():update(motion)
end
-- end
end
We said self.nextInvader
, not self:nextInvader
.
With that change, a fascinating thing happens. All the invaders draw, and just one of them, the topmost leftmost one, streaks across the screen. Something tells me we’re not ticking through all the invaders in update.
function Army:nextInvader()
if self.invaderNumber > #self.invaders then
self.invaderNumber = 1
end
local inv = self.invaders[invaderNumber]
self.invaderNumber = self.invaderNumber + 1
return inv
end
It seems that should be incrementing invaderNumber and returning a new invader each time. I do note that the update looks like this:
function Army:update()
-- if itIsTimeToUpdate() then
-- resetTimeToUpdate()
local continue = true
while(continue) do
continue = self:nextInvader():update(motion)
end
-- end
end
Invader:update
is supposed to return a boolean. It doesn’t. It should return true if it’s not alive. Let’s fix that but I think I need to write some tests.
function Invader:update(motion)
if self.alive then
self.pos = self.pos + motion
return false
else
return true
end
end
That changes nothing, the one square just flies off. I could debug that but I think some testing is called for here.
_:test("nextInvader returns unique instances", function()
local army = Army()
local i1 = army:nextInvader()
local i2 = army:nextInvader()
_:expect(i1).isnt(i2)
end)
4: nextInvader returns unique instances -- Actual: nil, Expected: nil
My first guess was that nextInvader
was somehow returning the same instance all the time. It seems, however, to return nil. Surely not. Look at it again:
function Army:nextInvader()
if self.invaderNumber > #self.invaders then
self.invaderNumber = 1
end
local inv = self.invaders[invaderNumber]
self.invaderNumber = self.invaderNumber + 1
return inv
end
I don’t see it. Assert on invader number:
_:test("invaderNumber starts at 1", function()
local army = Army()
_:expect(army.invaderNumber).is(1)
end)
That passes. Test to be sure it increments:
_:test("invaderNumber increments", function()
local army = Army()
_:expect(army.invaderNumber).is(1)
army:nextInvader()
_:expect(army.invaderNumber).is(2)
end)
Do these tests seem tiny? Yes. Why am I doing that? I’m doing that because I don’t yet see what is going on. If I were debugging, I’d put print statements or breakpoints at the positions that correspond to these ideas about what could go wrong, run, and see what happened.
I do the same thing with these tests, going in tiny steps, since my first big one, checking two instances for not being equal, surprised me by returning nils for both.
I’m going to write another trivial test just to keep the rhythm:
_:test("nextInvader returns an instance", function()
local army = Army()
local i1 = army:nextInvader()
_:expect(i1).isnt(nil)
end)
This fails. CodeaUnit’s message is a bit odd on isnt
:
6: nextInvader returns an instance -- Actual: nil, Expected: nil
What is up with that? Look again:
function Army:nextInvader()
if self.invaderNumber > #self.invaders then
self.invaderNumber = 1
end
local inv = self.invaders[invaderNumber]
self.invaderNumber = self.invaderNumber + 1
return inv
end
That’s gotta be non-nil. I’m going to assert on it in the code: I can’t think of a closer test to write.
function Army:nextInvader()
if self.invaderNumber > #self.invaders then
self.invaderNumber = 1
end
local inv = self.invaders[invaderNumber]
assert(inv ~= nil)
self.invaderNumber = self.invaderNumber + 1
return inv
end
The assertion asserts because inv is nil. I am sure glad of that. But why? It has to be that there’s no item 1 in self.invaders? A test:
_:test("there are invaders", function()
local army = Army()
local n = #army.invaders
_:expect(n).is(55)
end)
This passes. There are invaders. And yet the next invader code isn’t finding them. I should really take a break and talk to the cat, because I’m sure if I did the problem would pop into my head. But I’m not going to do the smart thing, I’m going to bear down on this problem.
_:test("there are invaders", function()
local army = Army()
local n = #army.invaders
_:expect(n).is(55)
local inv = #army.invaders[1]
_:expect(inv).isnt(nil)
end)
Let’s see if I can fetch one this way. Yes, that passes. What the heck am I missing? Do you see it? Some reader surely sees it. That’s why pairing is so useful.
More asserts, is all I can think of:
Ah!!!
function Army:nextInvader()
if self.invaderNumber > #self.invaders then
self.invaderNumber = 1
end
assert(#self.invaders == 55)
local inv = self.invaders[invaderNumber]
self.invaderNumber = self.invaderNumber + 1
return inv
end
The subscript should say self.invaderNumber
!!! Finally, putting that assert right in front of it and seeing it not assert, I noticed the lack of self
. A message from Lua about indexing with nil
would have been nice to have, but life is tough. Let’s run and see what we get now.
Whew! The invaders are marching again. I think we have successfully moved marching down into Army,
which was the point of the exercise.
Commit: Army handles marching.
I’m not entirely sure we’ve got all the group behavior moved but since Invaders
is an Army
now, we can be pretty sure no one is looping over it. And let’s rename that variable now, to … TheArmy, I guess.
Ah. In going through to change the name, I discovered that the missile has been looping over the invaders, and so since we moved to Army, missile hits don’t work. Here’s the code:
function drawMissile()
if Missile.v == 0 then return end
rect(Missile.pos.x, Missile.pos.y, 2,4)
Missile.pos = Missile.pos + vec2(0,0.5)
checkForKill(Missile)
end
function checkForKill(missile)
for i, invader in ipairs(TheArmy) do
if invader:killedBy(missile) then
missile.v = 0
end
end
end
We should refer this question to the Army, and have it return the true if any invader was killed:
function Army:checkForKill(missile)
for i, invader in ipairs(self.invaders) do
if invader:killedBy(missile) then
return true
end
end
return false
end
This nearly works but I noticed that dead invaders are coming back to life, in the wrong position. I suspect Invader:killedBy
:
function Invader:killedBy(missile)
self.alive = not self:isHit(missile)
return not self.alive
end
Wonderful! Remember that clever code? Well it just bit us in the tail. Perfect. Couldn’t be better had I planned it. Let’s code this again, sensibly.
function Invader:killedBy(missile)
if not self.alive then return false end
if self:isHit(missile) then
self.alive = false
return true
else
return false
end
end
That seems to do the job. Now we have completed the refactoring from our original procedural code to the Army
class, including still walking off the edge. We now have a better place to stand to do the reversal logic.
Commit: Army refactoring appears complete.
Yes, appears. I’m not dead certain, but I think we’re in good shape. I’ll need a fresh brain to take another look.
It’s 0915, my wife is leaving for work. Let’s sum up and maybe go get a chai.
Summing Up
We set out to move toward a design of there being an Army
class that held instances of an Invader
class, and we got there. We started with the Invader
class, and moved the logic we had into it, to display and check for hits.
Then we made the Invaders march right … off the screen. Before putting in the complex logic of reversal, we decided to create the intended Army class, to give us a better place to stand while doing that logic.
That went fairly well until we started to move the motion over, because a simple transcription error made us reference a nil subscript instead of a real one. That was hard for me to spot for some reason.
The upshot of that was that I wrote a lot of tests to check the next invader logic and once the simple defect was found, they all worked. The good news may be that I’ve rather broken through the no tests barrier, so perhaps further tests will seem easier to write.
Be that as it may, we have the invaders drawing and moving themselves, and we have fairly decent communication between the invaders, the army, and the missile. (We’ll need more missiles, I reckon, but one is a start.)
What have we learned? Oh, right, that clever code. I swear I didn’t plan that, but I couldn’t be happier that it resulted in invaders coming back to life. And it was fairly hard to detect that, not aided by the fact that missiles weren’t working at all and I had never tried them. Some tests for that would be handy, wouldn’t they?
But what poetic justice! Write two darn lines of clever code … and they don’t work! A perfect call for straightforward simple code.
I think I’d like to go out for a short top-down trip to the coffee kiosk and get a chai.
See you next time!