Space Invaders 39
Speed indicator; Bomb dropping order; Free player, maybe.
It’s Monday, 0820, a holiday here in the USA, which I suppose means I don’t have to go in to work, except that I never go in to work. I have three things in mind for today:
- An indicator of what the cycle time is;
- A start at emulating the original game’s bomb dropping order;
- Possibly a free player every 1000 points.
This is just a tentative plan, and if something pops up, we might do that instead. And, since a free player is probably easier than the bomb dropping order, I might do that before the bomb thing.
According to the Computer Archaeology material on Space Invaders, the game drops bombs in a fixed, regular order, from this column, then that, then the other, in a predictable cycle. And there can only be three bombs on screen at a time. And one of the bomb types, the “rolling” one, is always dropped directly over the gunner. I’ve not completely sussed out the rules: they’ll require more careful study.
Our missiles are dropped from random invaders, not columns, and the bomb type is chosen at random. Presently, we don’t even make sure the bomb drops from the bottom rank: it can fall through some invaders before reaching free space.
It follows that in order to look even close to the original, we’ll need to drop from the bottom of the column of invaders, and we should at least see that no more than three bombs are on screen at any given time. I think there is a discreet interval between bombs dropping in the original game, while ours can even drop two side-by side. Bombs are not dropped when the player has exploded, and there is a two-second period after the player spawn when no bombs are dropped.
So there’s a bit to do there, and we’ll try to do it in small bits.
As for the cycle speed, I’d just like to have a sense of whether the game is cycling at 1/120th, 1/60th, or 1/30th of a second. I have in mind a small indicator light or something like that.
Let’s start with that. I think I’ll just place a small dot, just below the green line, far off to the left, colored blue, green, or yellow depending on the cycle time. I think we should add it as part of drawStatus
:
function drawStatus()
pushStyle()
tint(0,255,0)
sprite(Line,8,16)
textMode(CORNER)
fontSize(10)
text(tostring(Lives), 24, 4)
text("SCORE " .. tostring(Score), 144, 4)
tint(0,255,0)
local addr = 40
for i = 0,Lives-2 do
sprite(asset.play,40+16*i,4)
end
if Lives == 0 then
textMode(CENTER)
text("GAME OVER", 112, 32)
end
popStyle()
end
Probably doesn’t matter where we put it, so we’ll put it last:
...
if Lives == 0 then
textMode(CENTER)
text("GAME OVER", 112, 32)
end
drawSpeedIndicator()
popStyle()
end
function drawSpeedIndicator()
ellipse(8,12,4)
end
Kind of a spike for the thing, and already I don’t like it. It shows up underneath the information panel that shows prints and test results, and although I’d close that if playing, I often leave it open while developing. We’ll move it over to the other side and then add the color:
function drawSpeedIndicator()
if DeltaTime < 0.016 then
fill(0,0,255)
elseif DeltaTime > 0.02 then
fill(255,255,0)
else
fill(0,255,0)
end
ellipse(214,12,4)
end
This draws a nice dot, and it turns out that taking a system screen shot slows Codea enough that I even get to see both blue and green on my fast iPad. Here’s what it looks like in blue:
So that’s nice. Commit: speed indicator.
There will now be a short break, because we have to go return some cans, get some cat food, and buy a chai for me and a coffee for my dear wife.
Wait right here. Or don’t, I’m not the boss of you.
Back, finally …
I hope you didn’t wait. My car, previously rated as 10/10 by me, acted up on the way to the store, so we brought it home and took my wife’s. Then things went well enough, we were able to turn in some cans, get the necessary cat food, and our chai/coffee respectively. Upon return, I researched the fault, posted for advice on the car’s forum, sent a note to my service advisor. Then, I checked a bit of Twitter. All that took about an hour.
What now? Let’s make a little progress on the bomb column stuff. I think if I were to do the free cannon stuff, I might stop when it was done, even though it’s easy. Let’s start on the bomb work instead.
Among the things we could do:
- Ensure no more than 3 bombs are on screen at a time;
- Ensure bombs drop from the lowest invader in a column;
- Make the dropping decision based on the column instead of per invader;
Where does the bomb stuff happen now? Let’s look. It happens in Invader:update
:
function Invader:update(motion, army)
if not self.alive then return true end
army:reportingForDuty()
self.picture = (self.picture+1)%2
self:possiblyDropBomb(army)
self.pos = self.pos + motion
Shield:checkForShieldDamage(self)
if self:overEdge() then
army:invaderOverEdge(self)
end
return false
end
function Invader:possiblyDropBomb(army)
if not self:isBottom(army) then return end
if math.random() < 0.98 then return end
self:dropBomb(army)
end
Based on the rules we’re going to try to emulate, as I understand them, this is really in the wrong place. In Army
we have this:
function Army:update()
updateGunner()
local continue = true
while(continue) do
continue = self:nextInvader():update(self.motion, self)
end
for b,bomb in pairs(self.bombs) do
b:update(self)
end
end
Now, in this update, we only update a single live invader, to emulate the limited speed of the original. (We do update all the bombs, except that they only move every third opportunity anyway.) This update is triggered by the GameRunner
:
function GameRunner:update()
self:updateAlways()
self:update60ths()
end
function GameRunner:update60ths()
self.time120 = self.time120 + (DeltaTime < 0.016 and 1 or 2)
if self.time120 < 2 then return end
self.time120 = 0
TheArmy:update()
end
This, in turn, gets called from Main
, on every cycle.
We are sure that Army:update
is called ever 60th of a second, unless we are on the world’s slowest iPad, so we should do the bomb stuff there. There’s nothing like it being done now, so we can put it into place more or less at will.
What do we want to do? We want to
- Be sure there is at least one bomb slot available;
- Select the column to drop from;
- Select the bomb type (plunger or squig)
- Decide the bomb’s y coordinate;
- Drop it.
We may have to change the ordering of these decisions when we do the targeted bomb, the rolling type. We’ll see.
What would it look like to TDD this?
Oh, wait, that reminds me. There’s a test that causes an explosion sound. I don’t like that. I want the sound player not to play any sounds during the tests. I’ll give it an optional flag:
function Sound:init(noisy)
function Sound:init(noisy)
if noisy == nil then
self.noisy = true
else
self.noisy = noisy
end
...
function Sound:play(aSoundName)
self:playRaw(self.sounds[aSoundName])
end
function Sound:playRaw(aSound)
if self.noisy then sound(aSound) end
end
OK, that took me a bit longer than I had in mind, too many “nots” in my head. Commit: silent tests.
That cost me some momentum. I should have made a note to do it later, but the sound was irritating.
Now then, bombs from columns. What might we want to TDD about this? I imagine we’ll have something like this:
function Army:update()
updateGunner()
local continue = true
while(continue) do
continue = self:nextInvader():update(self.motion, self)
end
possiblyDropBomb() -- <---
for b,bomb in pairs(self.bombs) do
b:update(self)
end
end
We already have the dropping and removing code in Army, because the Army tracks the bombs:
function Army:dropBomb(bomb)
self.bombs[bomb] = bomb
end
function Army:deleteBomb(bomb)
self.bombs[bomb] = nil
end
Let’s remove the bomb logic from the Invaders entirely. We can review it if need be but we don’t want two sources dropping them. We’ll drop the commented line below and the three functions beneath it:
function Invader:update(motion, army)
if not self.alive then return true end
army:reportingForDuty()
self.picture = (self.picture+1)%2
--self:possiblyDropBomb(army)
self.pos = self.pos + motion
Shield:checkForShieldDamage(self)
if self:overEdge() then
army:invaderOverEdge(self)
end
return false
end
function Invader:possiblyDropBomb(army)
if not self:isBottom(army) then return end
if math.random() < 0.98 then return end
self:dropBomb(army)
end
function Invader:isBottom()
return true
end
function Invader:dropBomb(army)
army:dropBomb(Bomb(self.pos - vec2(-8,16 + math.random(0,3))))
end
The game is easier now that there are no bombs. Commit: remove invader bombs.
Notably, no tests fail, which tells us something about how well tested the old scheme was.
Testing this is odd. I really feel that I could just write it. Let’s try to really push it. I’ll start testing and set up situations in Army where we should, or should not, drop a bomb. That will force it either to be purely deterministic, or we’ll have to control the random number generator. I believe in the original game, if it can drop a bomb, it does. Except if there’s no cannon alive and for two seconds thereafter. Let’s test for the first and ignore the two second rule for now.
OK, I’m going in to write a test.
_:test("drop three and only three bombs at a time", function()
local theArmy = Army()
_:expect(bombCount()).is(0)
theArmy:possiblyDropBomb()
_:expect(bombCount()).is(1)
theArmy:possiblyDropBomb()
_:expect(bombCount()).is(2)
theArmy:possiblyDropBomb()
_:expect(bombCount()).is(3)
theArmy:possiblyDropBomb()
_:expect(bombCount()).is(3)
end)
I’m not entirely happy with this but I do think it’s likely to stay correct. We’re definitely designing as we go here–that’s what TDD is about, you know–so if it changes that’s OK too.
Now I need to write bombCount(), a local function in the testing tab. No, wait, let’s give that to Army, because it’ll be needed there. Change the test:
_:test("drop three and only three bombs at a time", function()
local theArmy = Army()
_:expect(theArmy:bombCount()).is(0)
theArmy:possiblyDropBomb()
_:expect(theArmy:bombCount()).is(1)
theArmy:possiblyDropBomb()
_:expect(theArmy:bombCount()).is(2)
theArmy:possiblyDropBomb()
_:expect(theArmy:bombCount()).is(3)
theArmy:possiblyDropBomb()
_:expect(theArmy:bombCount()).is(3)
end)
Sweet. We know why that’ll fail.
16: drop three and only three bombs at a time -- Tests:149: attempt to call a nil value (method 'bombCount')
function Army:bombCount()
local c = 0
for k,v in pairs(self.bombs) do
c = c + 1
end
return c
end
I could imagine keeping track as we add and remove them but this is certain to work. Now we’ll fail on the other missing method:
16: drop three and only three bombs at a time -- Tests:150: attempt to call a nil value (method 'possiblyDropBomb')
We’ll code that to drop a bomb unconditionally:
function Army:possiblyDropBomb()
self:dropBomb(Bomb())
end
Now I expect the test to fail on the second check for three.
16: drop three and only three bombs at a time -- Actual: 4, Expected: 3
So now we need to fix “possibly” a bit:
function Army:possiblyDropBomb()
if self:bombCount() < 3 then self:dropBomb(Bomb()) end
end
And our test passes. Sweet. What else do we want to check at this time? Let me review the code comments from the Computer Archaeology site for some guidance. Here’s one relevant bit:
Game Object 2, 3, and 4: Alien Shots
There are three different alien shots in the game, and each has a unique picture. Object 2 is the “Rolling” shot. It spirals a bit as it falls. Object 3 is the “Plunger” shot. It reminds me of a bathroom plunger falling down the screen. Object 4 is the “Squiggly” shot. It looks like a turning zig-zag line much like a resistor circuit symbol.
The byte timer from Object 2 is used to synchronize the three shots so that only one shot is processed per screen. The objects can’t synchronize directly from the timer byte at 2032 since its decrement depends on which half of the screen it is on. Instead the timer byte is copied to a sync-flag at 0x2080 where it remains constant for all the shots between end and mid screen interrupts.
The code uses a common routine to handle all three shots. Each shot task copies its data structure to a common area, calls the handler, and then copies the changed data back to its personal area.
The common handler is responsible for moving each shot. The normal delta Y for the shots is a constant 4 pixels down per step. A shot makes a step every 3 frames. (460/3 = 80 pixels per second). But when there are 8 or fewer aliens on the screen the delta changes up to 5 pixels per step (560/3 = 100 pixels per second).
The common handler also initiates the alien shots. Each shot has a move-counter that starts when the shot is dropped and counts up with each step as it falls. The game keeps a constant reload rate that determines how fast the aliens can fire. The game takes the smallest count of the other two shots and compares it to the reload rate. If it is too soon since the last shot then no shot is fired.
The reload rate gets faster as the game progresses. The code uses the upper two digits of the player’s score as a lookup into the table at 1AA1 and sets the reload rate to the value in the table at 1CB8. The reload rate for anything below 0x0200 is 0x30. From 0x0200 to 0x1000 the delay drops to 0x10, then 0x0B at from 0x1000 to 0x2000. From 0x2000 to 0x3000 the rate is 8 and then maxes out above 0x3000 at 7. With a little flying-saucer-luck you will reach 0x3000 in 2 or 3 racks.
The “rolling” shot (object 2) drops right over the player when it falls. The other two shots use pointers into a table of columns at 0x1D00. The pointers advance very predictably. You can see from the table that the “squiggly” shot will fall first in column 11 and then 1, 6, and 3.
When there is only one alien left on the screen the plunger shot is disabled. Only the “rolling” (tracking) and squiggly shots are active, which means the tracking shot gets fired more often.
Here is the table of columns that is referenced:
ColFireTable:
; This table decides which column a shot will fall from. The column number is read from the
; table (1-11) and the pointer increases for the shot type. For instance, the "squiggly" shot
; will fall from columns in this order: 0B, 01, 06, 03. If you play the game you'll see that
; order.
;
; The "plunger" shot uses index 00-0F (inclusive)
; The "squiggly" shot uses index 06-14 (inclusive)
; The "rolling" shot targets the player
1D00: 01 07 01 01 01 04 0B 01 06 03 01 01 0B 09 02 08
1D10: 02 0B 04 07 0A
The comment says that the squiggly shot uses 06-14(hex) and will first fall in column 11(decimal). We find Bhex=11dec in column six of this table, so I think I understand it.
Since the original game just has three slots for bombs, it seems that each slot has a fixed bomb type in it and, I guess, it just ticks through the types one after another. The table order is rolling, plunger, and squiggly, I gather. That makes me think we should see things in that order at the beginning of a game.
In my favorite game to watch for analysis, I see a rolling, pretty well aimed at the player, then a plunger from column 1, then a squiggly from 11, then a squiggly from 1, then a rolling (aimed) from 2, then plunger, plunger, squiggly, squiggly, rolling. That’s not what I’m expecting. I’ll have to read the code (and comments) in more detail.
It looks like the drop of the rolling one still starts from the center of a column, and that it decides to drop as soon as the bomb is above the cannon, even if it might hit a shield. Early in the game, there aren’t even three on the screen at the same time, probably because of the shot timer, which is score dependent. That’ll definitely be another story.
The initial reload rate is 30: that’s thirty moves of the slower of the (up to) two bombs on screen.
I don’t want to invest a lot in the bomb selection logic, so for now let’s just do this: we’ll begin cycling through the types rolling, plunger, squiggly.
I’m inclined to drop the notion of the table that could contain any number of bombs, and go to some approach with just the three types, one of each. I’m not sure I’ll reuse the final table for the saucer as does the original game: we’ll leave that decision for saucer day.
Do we want three discrete variables bomb1, 2 3, or a table with a known index? Let’s actually go for the three variables for now. We will then know what kind of bomb we’re looking at and we can give them additional info as we need it.
OK, in Army’s init, I’ll make three bombs. I’m not going to TDD that: my theory is that I can get it right.
Bombs presently self-init this way:
function Bomb:init(pos)
self.pos = pos
self.shapes = BombTypes[math.random(3)]
self.shape = 0
self.cycle = 2
self.explodeCount = 0
end
We’ll want to specify the shape, and probably not the position, yet. I’m going to ignore cycle, because I really don’t care when they update.
Right now, BombTypes are created as part of setting up:
function createBombTypes()
BombTypes = {
{readImage(asset.plunger1), readImage(asset.plunger2), readImage(asset.plunger3), readImage(asset.plunger4)},
{readImage(asset.rolling1), readImage(asset.rolling2), readImage(asset.rolling3), readImage(asset.rolling4)},
{readImage(asset.squig1), readImage(asset.squig2), readImage(asset.squig3), readImage(asset.squig4)}
}
BombExplosion = readImage(asset.alien_shot_exploding)
end
I think we’ll want to move all that directly into Army, with some mods:
function Army:defineBombs()
local bombTypes = {
{readImage(asset.rolling1), readImage(asset.rolling2), readImage(asset.rolling3), readImage(asset.rolling4)},
{readImage(asset.plunger1), readImage(asset.plunger2), readImage(asset.plunger3), readImage(asset.plunger4)},
{readImage(asset.squig1), readImage(asset.squig2), readImage(asset.squig3), readImage(asset.squig4)}
}
local bombExplosion = readImage(asset.alien_shot_exploding)
self.rollingBomb = Bomb(bombTypes[1], bombExplosion)
self.plungerBomb = Bomb(bombTypes[2], bombExplosion)
self.squiggleBomb = Bomb(bombTypes[3], bombExplosion)
end
I decided that I’d better pass the explosion sprite to the bomb. While I’m thinking of it, I’m going to update the drawing for bombs, but I have no way to test it. Still seems the right thing to do:
function Bomb:init(shapes, explosion)
self.pos = pos
self.shapes = shapes
self.explosion = explosion
self.shape = 0
self.cycle = 2
self.explodeCount = 0
end
function Bomb:draw()
if self.explodeCount > 0 then
sprite(self.explosion, self.pos.x, self.pos.y)
self.explodeCount = self.explodeCount - 1
if self.explodeCount == 0 then self.army:deleteBomb(self) end
else
sprite(self.shapes[self.shape + 1],self.pos.x,self.pos.y)
end
end
This is now pretty broken, but I think we can get it back to life pretty quickly. First, let’s remove the bombs tables and break the test.
16: drop three and only three bombs at a time -- Army:45: bad argument #1 to 'for iterator' (table expected, got nil)
Right, no table. We need to make two things happen. First, we need to make bombs get added to our three variables one after another, and second, we need to have some way to know if there’s still one there. This logic may get more complex: we know, for example, that we only use two types late in the game. For now …
function Army:bombCount()
local c = 0
if self.rollingBomb.alive then c = c + 1 end
if self.plungerBomb.alive then c = c + 1 end
if self.squiggleBomb.alive then c = c + 1 end
return c
end
I’ve just arbitrarily decided to have an alive flag. Let’s init that to false in Bomb:
function Bomb:init(shapes, explosion)
self.pos = pos
self.shapes = shapes
self.explosion = explosion
self.shape = 0
self.cycle = 2
self.alive = false
self.explodeCount = 0
end
Now the non-zero count tests should fail and the zero one pass, I think.
Two tests fail, the one I expected, but not quite as I expected, and another one:
16: drop three and only three bombs at a time -- Army:52: attempt to call a nil value (method 'dropBomb')
9: Bomb hits gunner -- Main:98: bad argument #-2 to '__add' (vec2)
Test 9 looks like this:
_:test("Bomb hits gunner", function()
setupGunner()
Gunner.pos=vec2(50,50)
-- gunner's rectangle is 16x8 on CORNER
-- covers x = 50-65 y = 50,57
local bomb = Bomb(vec2(50,50))
-- bomb is 3x4, covers x = 50-52 y = 50-53
_:expect(bomb:killsGunner()).is(true)
bomb.pos.y = 58
Gunner.alive = true
_:expect(bomb:killsGunner()).is(false)
bomb.pos.y = 57
...
And on and on. Based on that, I propose to accept a pos parameter after all, and to init it to zero in the real versions. I’ll spare you the code for that, it’s straightforward. Now we’re down to the almost-expected failure, the missing dropBomb
call. Let’s do this:
function Army:dropBomb()
if not self.rollingBomb.alive then
self.rollingBomb.alive = true
elseif not self.plungerBomb.alive then
self.plungerBomb.alive = true
elseif not self.squiggleBomb.alive then
self.squiggleBomb.alive = true
end
end
That’s enough to make our current test pass, I hope.
And it is, but the game crashes, in draw:
Army:118: bad argument #1 to 'for iterator' (table expected, got nil)
stack traceback:
[C]: in function 'next'
Army:118: in method 'draw'
GameRunner:21: in method 'draw'
Main:25: in function 'draw'
No surprise there, we’re not drawing them. Let’s fix that, at least kind of:
function Army:draw()
pushMatrix()
pushStyle()
for i,invader in ipairs(self.invaders) do
invader:draw()
end
if self.rollingBomb.alive then self.rollingBomb:draw() end
if self.plungerBomb.alive then self.plungerBomb:draw() end
if self.squiggleBomb.alive then self.squiggleBomb:draw() end
popStyle()
popMatrix()
end
I really feel odd about having just these three bombs. Every instinct I’ve developed tells me to put them in a table or something more general than three scalar variables. However, at least at this point, I think it’s going to be easier to emulate the original game’s behavior with the scalars. We’ll see about improving when we have it working.
We now crash in update:
Army:71: bad argument #1 to 'for iterator' (table expected, got nil)
stack traceback:
[C]: in function 'next'
Army:71: in method 'update'
GameRunner:42: in method 'update60ths'
GameRunner:31: in method 'update'
Main:28: in function 'draw'
Probably not a surprise if we were looking ahead, but we’re letting failures drive us, just as we would with tests. I hope to get back to tests in a moment, but I want the game not to crash.
function Army:update()
updateGunner()
local continue = true
while(continue) do
continue = self:nextInvader():update(self.motion, self)
end
for b,bomb in pairs(self.bombs) do
b:update(self)
end
end
Yes, well.
function Army:update()
updateGunner()
local continue = true
while(continue) do
continue = self:nextInvader():update(self.motion, self)
end
self.rollingBomb:update()
self.plungerBomb:update()
self.squiggleBomb:update()
end
We’d better glance at Bomb:update
:
function Bomb:update(army, increment)
if self.explodeCount > 0 then return end
self.cycle = (self.cycle +1)%3
if self.cycle ~= 0 then return end
self.pos.y = self.pos.y - (increment or 4)
self.shape = (self.shape + 1)%4
self:checkCollisions(army)
end
Yes well. We’d better pass in the army. The increment is optional and I’m not sure if anyone is still using it. For now, leave it. We want to early out if not alive:
function Bomb:update(army, increment)
if not self.alive then return end
if self.explodeCount > 0 then return end
self.cycle = (self.cycle +1)%3
if self.cycle ~= 0 then return end
self.pos.y = self.pos.y - (increment or 4)
self.shape = (self.shape + 1)%4
self:checkCollisions(army)
end
function Army:update()
updateGunner()
local continue = true
while(continue) do
continue = self:nextInvader():update(self.motion, self)
end
self.rollingBomb:update(self)
self.plungerBomb:update(self)
self.squiggleBomb:update(self)
end
Game now runs OK except for not dropping any bombs yet. No surprise there. But when we fire a missile, we crash, trying to see if the missile has hit a bomb:
Army:138: bad argument #1 to 'for iterator' (table expected, got nil)
stack traceback:
[C]: in function 'next'
Army:138: in method 'checkForKill'
Army:127: in method 'processMissile'
Missile:26: in method 'draw'
Gunner:42: in function 'drawGunner'
GameRunner:22: in method 'draw'
Main:25: in function 'draw'
function Army:checkForKill(missile)
for i, invader in ipairs(self.invaders) do
if invader:killedBy(missile) then
missile.v = 0
return
end
end
for b,bomb in pairs(self.bombs) do
if bomb:killedBy(missile) then
missile.v = 0
bomb:explode(self)
return
end
end
end
I’m going to write this out longhand but I’m not going to like it:
function Army:checkForKill(missile)
for i, invader in ipairs(self.invaders) do
if invader:killedBy(missile) then
missile.v = 0
return
end
end
if self.rollingBomb:killedBy(missile) then
missile.v = 0
self.rollingBomb:explode(self)
elseif self.plungerBomb:killedBy(missile) then
missile.v = 0
self.plungerBomb:explode(self)
elseif self.squiggleBomb:killedBy(missile) then
missile.v = 0
self.squiggleBomb:explode(self)
end
end
Yum, duplication. I told you I wasn’t going to like it. But we have our implementation hat on, not our refactoring hat, so let’s go ahead and see what happens now. I expect tests to run game to play, and my missiles still kill invaders.
And, mysteriously, that’s what happens.
Next step, I reckon, is to make the dropBomb code actually do something. Let’s see if we can test-drive that. I think I’ll change the existing test to be more stringent.
_:test("drop three and only three bombs at a time", function()
local theArmy = Army()
_:expect(theArmy:bombCount()).is(0)
theArmy:possiblyDropBomb()
_:expect(theArmy:bombCount()).is(1)
_:expect(theArmy.rollingBomb.alive).is(true)
theArmy:possiblyDropBomb()
_:expect(theArmy:bombCount()).is(2)
_:expect(theArmy.plungerBomb.alive).is(true)
theArmy:possiblyDropBomb()
_:expect(theArmy:bombCount()).is(3)
_:expect(theArmy.squiggleBomb.alive).is(true)
theArmy:possiblyDropBomb()
_:expect(theArmy:bombCount()).is(3)
end)
Sweet. That now checks the order of play for bombs as well as ensuring that there can only be three. Of course, we now know that there can be only three because there are only three, so we can remove that check and the bombCount method. Yeah, let’s do that.
_:test("drop three and only three bombs at a time", function()
local theArmy = Army()
_:expect(theArmy:bombCount()).is(0)
theArmy:possiblyDropBomb()
_:expect(theArmy.rollingBomb.alive).is(true)
theArmy:possiblyDropBomb()
_:expect(theArmy.plungerBomb.alive).is(true)
theArmy:possiblyDropBomb()
_:expect(theArmy.squiggleBomb.alive).is(true)
end)
Now in principle we no longer know that the other bombs are still active. Let’s check them all each time just to be sure:
_:test("drop three and only three bombs at a time", function()
local theArmy = Army()
_:expect(theArmy:bombCount()).is(0)
theArmy:possiblyDropBomb()
_:expect(theArmy.rollingBomb.alive).is(true)
_:expect(theArmy.plungerBomb.alive).is(false)
_:expect(theArmy.squiggleBomb.alive).is(false)
theArmy:possiblyDropBomb()
_:expect(theArmy.rollingBomb.alive).is(true)
_:expect(theArmy.plungerBomb.alive).is(true)
_:expect(theArmy.squiggleBomb.alive).is(false)
theArmy:possiblyDropBomb()
_:expect(theArmy.squiggleBomb.alive).is(true)
_:expect(theArmy.rollingBomb.alive).is(true)
_:expect(theArmy.plungerBomb.alive).is(true)
end)
Oh, I’ve still got one call to bombCount. That reminds me to remove it and fix that part of the test.
16: drop three and only three bombs at a time -- Army:44: attempt to call a nil value (method 'bombCount')
Hmm, forgot that:
function Army:possiblyDropBomb()
if self:bombCount() < 3 then self:dropBomb(Bomb()) end
end
Hm. Can’t we just remove that and call dropBomb
, which already checks the three slots? I think we can, but the name possibly is better, so we’ll remove this and rename the other.
function Army:possiblyDropBomb()
if not self.rollingBomb.alive then
self.rollingBomb.alive = true
elseif not self.plungerBomb.alive then
self.plungerBomb.alive = true
elseif not self.squiggleBomb.alive then
self.squiggleBomb.alive = true
end
end
And the test now is:
_:test("drop three and only three bombs at a time", function()
local theArmy = Army()
_:expect(theArmy.rollingBomb.alive).is(false)
_:expect(theArmy.plungerBomb.alive).is(false)
_:expect(theArmy.squiggleBomb.alive).is(false)
theArmy:possiblyDropBomb()
_:expect(theArmy.rollingBomb.alive).is(true)
_:expect(theArmy.plungerBomb.alive).is(false)
_:expect(theArmy.squiggleBomb.alive).is(false)
theArmy:possiblyDropBomb()
_:expect(theArmy.rollingBomb.alive).is(true)
_:expect(theArmy.plungerBomb.alive).is(true)
_:expect(theArmy.squiggleBomb.alive).is(false)
theArmy:possiblyDropBomb()
_:expect(theArmy.squiggleBomb.alive).is(true)
_:expect(theArmy.rollingBomb.alive).is(true)
_:expect(theArmy.plungerBomb.alive).is(true)
end)
That’s long and verbose. It also runs correctly, which I see as a big plus.
Now we need to drop the bombs, which says to me that we need to pick a column for each one and give it a position and velocity. Then we’ll need to kill it at the other end, which is probably going to happen with a crash. Let’s make the possiblyDropBomb
code do something sensible:
function Army:possiblyDropBomb()
if not self.rollingBomb.alive then
self:dropBomb(self.rollingBomb)
elseif not self.plungerBomb.alive then
self:dropBomb(self.plungerBomb)
elseif not self.squiggleBomb.alive then
self:dropBomb(self.squiggleBomb)
end
end
function Army:dropBomb(aBomb)
aBomb.alive = true
aBomb.pos = vec2(200,112)
end
I think this will drop bombs rather quickly from that position. We’ll have to refine that. Nothing happens. Maybe we have to call it?
function Army:update()
updateGunner()
local continue = true
while(continue) do
continue = self:nextInvader():update(self.motion, self)
end
self.rollingBomb:update(self)
self.plungerBomb:update(self)
self.squiggleBomb:update(self)
end
Oh yes, how about right after updating the gunner? They will drop one after another on every cycle. That’ll be weird but will tell me we’re wired up.
function Army:update()
updateGunner()
self:possiblyDropBomb()
local continue = true
while(continue) do
continue = self:nextInvader():update(self.motion, self)
end
self.rollingBomb:update(self)
self.plungerBomb:update(self)
self.squiggleBomb:update(self)
end
OK that sort of works. I have x and y reversed and let’s randomize x anyway:
function Army:dropBomb(aBomb)
aBomb.alive = true
aBomb.pos = vec2(112+math.random(-112,112), 200)
end
This will drop three bombs and then crash for inability to get rid of them.
There they are, and the message is:
Bomb:20: attempt to call a nil value (method 'deleteBomb')
stack traceback:
Bomb:20: in method 'draw'
Army:114: in method 'draw'
GameRunner:21: in method 'draw'
Main:25: in function 'draw'
Yes. Odd that that’s happening in draw, isn’t it? The code is this:
function Bomb:draw()
if self.explodeCount > 0 then
sprite(self.explosion, self.pos.x, self.pos.y)
self.explodeCount = self.explodeCount - 1
if self.explodeCount == 0 then self.army:deleteBomb(self) end
else
sprite(self.shapes[self.shape + 1],self.pos.x,self.pos.y)
end
end
That got further than I expected. Let’s implement deleteBomb in the simplest possible way:
function Army:deleteBomb(aBomb)
aBomb.alive = false
end
Now I expect bombs to rain down continually, but it seems they might correctly file explosions. We’ll see.
Sure enough they just rain down. Now we need to implement the refractory period between firing. We ought to return to testing. Our existing test thinks it can fire consecutively. And that’s not going to work. Let’s begin with a new test that does better.
My first test sketch is this:
_:test("bombs have delay between firing", function()
local theArmy = Army()
theArmy:possiblyDropBomb()
_:expect(theArmy.rollingBomb.alive).is(false)
theArmy:updateCycleTime(enough)
theArmy:possiblyDropBomb()
_:expect(theArmy.rollingBomb.alive).is(true)
end)
The idea is that the bomb won’t drop right away but after we updateCycleTime enough, it will. That’s definitely going to mess up our other test. We’ll deal with that in a moment. But here, I’m not really interested in testing what’s dropped, I want to know if things can be dropped. So let’s posit a new method, canDropBomb()
and test it directly. We’ll implement it to be used by possiblyDropBomb
, where the timing behavior is needed.
_:test("bombs have delay between firing", function()
local enough = 100
local theArmy = Army()
_:expect(theArmy:canDropBomb()).is(false)
theArmy:updateCycleTime(enough)
_:expect(theArmy:canDropBomb()).is(true)
end)
That should drive me somewhere nearly good:
17: bombs have delay between firing -- Tests:169: attempt to call a nil value (method 'canDropBomb')
As expected.
function Army:canDropBomb()
return self.bombCycle > self.bombDropCycleLimit
end
With an init, and the new method to update it:
function Army:updateBombCycle(anAmount)
self.bombCycle = self.bombCycle + anAmount or 1
end
And rename the method in the test because I have a new one I like better:
_:test("bombs have delay between firing", function()
local enough = 100
local theArmy = Army()
_:expect(theArmy:canDropBomb()).is(false)
theArmy:updateBombCycle(enough)
_:expect(theArmy:canDropBomb()).is(true)
end)
And this test runs. If we were to call this function in possiblyDropBomb
right now, we’d never drop a bomb again.
function Army:possiblyDropBomb()
if not self:canDropBomb() then return end
if not self.rollingBomb.alive then
self:dropBomb(self.rollingBomb)
elseif not self.plungerBomb.alive then
self:dropBomb(self.plungerBomb)
elseif not self.squiggleBomb.alive then
self:dropBomb(self.squiggleBomb)
end
end
I expect no bombs to fall in the game now, and indeed that happens. I get failures from the other test (and I note that the display from Codea is displaying assert failures not test failures. I’ll ignore that test for now but will make a point of fixing it later.
OK, all tests run, with one ignored. I think it’s supposed to update once per bomb update, but I’m not sure if that’s once each or once for all. For now, we’ll put it ahead of the bomb update. No, it goes better here:
function Army:update()
updateGunner()
self:updateBombCycle()
self:possiblyDropBomb()
local continue = true
while(continue) do
continue = self:nextInvader():update(self.motion, self)
end
self.rollingBomb:update(self)
self.plungerBomb:update(self)
self.squiggleBomb:update(self)
end
This should give us about one bomb per second, a bit more (50 60ths). Not quite, for a couple of reasons.
First, this:
function Army:updateBombCycle(anAmount)
self.bombCycle = self.bombCycle + (anAmount or 1)
end
The parens are necessary. Second, if we drop a bomb, we need to set the cycle back to zero.
function Army:dropBomb(aBomb)
self.bombCycle = 0
aBomb.alive = true
aBomb.pos = vec2(112+math.random(-112,112), 200)
end
That’s about right. Of course, they’re dropping from random locations. Let’s add a member value to the bomb to keep track of the index into the table that it’s supposed to use, and look up the column index it should use. We’ll have more to do after that, like finding where the columns are.
I think we might be able to write a test for this. I’ll paste the table into the test tab as a comment and work from it. It’s not clear to me who should be deciding about the column index and column number. It’s clearly a function of the bomb type, and of the number of times that bomb has been dropped. That sounds like the Army should ask the bomb, when it goes to fire it, what column to use. (We’ll deal with the targeted bomb separately.)
So we have the drop function above. I think it should be sending a message to the bomb to go, rather than jamming values into it as it is now.
Let’s therefore imagine the bomb getting a drop
message, and asking the Army for a position from which to drop, providing the desired column number. The army will know where that column is and how far down in it to start the drop.
So how to TDD this. It almost seems to call for some kind of test double, as the calls go back and forth between bomb and army. And we want our test to behave like the Army, I think.
Let’s sketch the test …
--; The "plunger" shot uses index 00-0F (inclusive)
--; The "squiggly" shot uses index 06-14 (inclusive)
--; The "rolling" shot targets the player
--1D00: 01 07 01 01 01 04 0B 01 06 03 01 01 0B 09 02 08
--1D10: 02 0B 04 07 0A
_:test("bombs select correct columns", function()
local theArmy = Army()
local plungerBomb = theArmy.plungerBomb
_:expect(plungerBomb:nextColumn()).is(0x01)
_:expect(plungerBomb:nextColumn()).is(0x07)
end)
That might do. Let’s run it, fail it, and see what we can drive out.
18: bombs select correct columns -- Tests:183: attempt to call a nil value (method 'nextColumn')
No surprise there, we need nextColumn. Fake it till you make it says it returns 0x01.
function Bomb:nextColumn()
return 0x01
end
18: bombs select correct columns -- Actual: 1, Expected: 7
Yes, well. We’d better init a column index and use it to look up in our table. I think we’ll just know the table in bomb for now:
function Bomb:init(pos, shapes, explosion)
self.pos = pos
self.shapes = shapes
self.explosion = explosion
self.shape = 0
self.cycle = 2
self.alive = false
self.explodeCount = 0
self.columnTable = {0x01, 0x07, 0x01, 0x01, 0x01, 0x04, 0x0B, 0x01, 0x06, 0x03, 0x01, 0x01, 0x0B, 0x09, 0x02, 0x08, 0x02, 0x0B, 0x04, 0x07, 0x0A}
self.columnIndex = 0
end
Then we’ll increment the index and access the table using it:
function Bomb:nextColumn()
local next = self.columnTable[self.columnIndex+1]
self.columnTable = (self.columnTable+1)%16
return next
end
I’m calling my shot that the test runs, but I confess I’m not very confident. Duh:
18: bombs select correct columns -- Bomb:91: attempt to perform arithmetic on a table value (field 'columnTable')
Increment the index, fool, not the table. In my defense, Codea offered the option, and I took it.
function Bomb:nextColumn()
local next = self.columnTable[self.columnIndex+1]
self.columnIndex = (self.columnIndex+1)%16
return next
end
Test now runs. Two more things to check, wrap-around, and the squiggly shot starting wherever it’s supposed to start, 11 or something.
_:test("bombs select correct columns", function()
local theArmy = Army()
local plungerBomb = theArmy.plungerBomb
_:expect(plungerBomb:nextColumn()).is(0x01)
_:expect(plungerBomb:nextColumn()).is(0x07)
for i = 1,13 do
plungerBomb:nextColumn()
end
_:expect(plungerBomb:nextColumn()).is(0x08)
_:expect(plungerBomb:nextColumn()).is(0x01)
end)
This works. Now for squiggly. For this to work, the bombs need an offset into the table and squiggly’s has to be initialized to the right value.
First the simple test on squiggly.
_:test("squiggly bombs select correct columns", function()
local theArmy = Army()
local squigglyBomb = theArmy.squigglyBomb
_:expect(squigglyBomb:nextColumn()).is(0x0b)
end)
This will fail returning 1, I fondly hope. But no:
19: squiggly bombs select correct columns -- Tests:195: attempt to index a nil value (local 'squigglyBomb')
Why’s squigglyBomb nil? Ah, it’s called squiggleBomb
. After judicious renaming:
19: squiggle bombs select correct columns -- Actual: 1, Expected: 11
We need now to provide an offset init for the bombs, whose present creation is this:
function Bomb:init(pos, shapes, explosion)
self.pos = pos
self.shapes = shapes
self.explosion = explosion
self.shape = 0
self.cycle = 2
self.alive = false
self.explodeCount = 0
self.columnTable = {0x01, 0x07, 0x01, 0x01, 0x01, 0x04, 0x0B, 0x01, 0x06, 0x03, 0x01, 0x01, 0x0B, 0x09, 0x02, 0x08, 0x02, 0x0B, 0x04, 0x07, 0x0A}
self.columnIndex = 0
end
I guess we’ll just put columnOffset last:
function Bomb:init(pos, shapes, explosion, columnOffset)
self.columnOffset = columnOffset
...
function Bomb:nextColumn()
local next = self.columnTable[self.columnOffset + self.columnIndex+1]
self.columnIndex = (self.columnIndex+1)%16
return next
end
And the initialization calls:
function Army:defineBombs()
local bombTypes = {
{readImage(asset.rolling1), readImage(asset.rolling2), readImage(asset.rolling3), readImage(asset.rolling4)},
{readImage(asset.plunger1), readImage(asset.plunger2), readImage(asset.plunger3), readImage(asset.plunger4)},
{readImage(asset.squig1), readImage(asset.squig2), readImage(asset.squig3), readImage(asset.squig4)}
}
local bombExplosion = readImage(asset.alien_shot_exploding)
self.rollingBomb = Bomb(vec2(0,0), bombTypes[1], bombExplosion, 0)
self.plungerBomb = Bomb(vec2(0,0), bombTypes[2], bombExplosion, 0)
self.squiggleBomb = Bomb(vec2(0,0), bombTypes[3], bombExplosion, 6)
end
I have high hopes for this.
And the squiggle bomb test passes. Let’s just try a couple more values for completeness.
_:test("squiggle bombs select correct columns", function()
local theArmy = Army()
local squiggleBomb = theArmy.squiggleBomb
_:expect(squiggleBomb:nextColumn()).is(0x0b)
_:expect(squiggleBomb:nextColumn()).is(0x01)
_:expect(squiggleBomb:nextColumn()).is(0x06)
_:expect(squiggleBomb:nextColumn()).is(0x03)
end)
That passes. We’re not using the nextColumn
function yet, and we need to do so.
One way, not the best way but expedient, will be to call it from Army when we fire a shot:
function Army:dropBomb(aBomb)
self.bombCycle = 0
aBomb.alive = true
aBomb.pos = vec2(112+math.random(-112,112), 200)
end
This code doesn’t know or care which bomb it is, it just wants to decide where to put it. Let’s fetch the column index, thus incrementing it), and use it to compute our x coordinate.
Arrgh. What I’m discovering with this is that the order of the invaders in my invaders table isn’t as useful for this purpose as it might be. We create them this way:
function Army:init()
local vader11 = readImage(asset.inv11)
local vader12 = readImage(asset.inv12)
local vader21 = readImage(asset.inv21)
local vader22 = readImage(asset.inv22)
local vader31 = readImage(asset.inv31)
local vader32 = readImage(asset.inv32)
local vaders = {
{vader31,vader32},
{vader21,vader22}, {vader21,vader22},
{vader11,vader12}, {vader11,vader12}
}
local scores = {30,20,20,10,10}
self.invaders = {}
for row = 1,5 do
for col = 1,11 do
local p = vec2(col*16, 185-row*16)
table.insert(self.invaders, 1, Invader(p,vaders[row], scores[row], army))
end
end
So that’s starting with the type 3 guys, then the type 2, then the type 1, inserting them at the beginning of the invaders table. But that means that the left most bottom-most invader is at the 11th position in the table. How nice.
I think the order is done that way because it looked best during updating. For now. Let’s finesse that issue this way:
function Army:dropBomb(aBomb)
self.bombCycle = 0
aBomb.alive = true
local col = aBomb:nextColumn()
local leader = self.invaders[11]
local leaderPos = leader.pos
local leaderX = leaderPos.x
local bombX = 8 + leaderX + (col-1)*16
aBomb.pos = vec2(bombX, leaderPos.y - 16)
end
Let’s see how this looks. I expect some changing around, with rolling and plungers coming from the same column.
I’m seeing a pattern that might be right, but never seeing a squiggle. I think what’s happening is that the plunger or roller dies, and since we don’t do a squiggle unless both the plunger and roller are active, we never drop a squiggle.
We probably want to try a roller first on the first attempt, a plunger on the second, and a squiggle on the third, and so on. That’s going to be tricky to implement, I think, given that I just moved away from having a table, to scalar values for the three bomb types.
Anyway, this has run quite long, and the game is still playable. We have an ignored test, but it’s time to end the working day. I’ll file this report, and take up my sword and shield again tomorrow.
Commit: bombs dropped from army. incomplete but working. one ignored test. Bombs still limited to about one per second.
Let’s sum up a bit before I fold my tent.
Summing Up
This took a while, but I was distracted by events around the house, it being Labor Day and all.
I think the TDD tests helped me get my ideas in better shape. Sometimes I have a good idea where I’m going with the code, and sometimes not. Today was a not. When that’s the case, TDD is particularly useful, because I have to ask myself, well, self, what do you want to have happen? Then I write tests that answer that question.
The situation was tricky for a number of reasons. I decided to move the bomb dropping from the invaders, where it was easy to do randomly but clearly wrong from the viewpoint of the original program. That induced me to rip out the old code and replace with new. It would perhaps have been better to refactor the existing code over to Army, but it seemed to me that we wanted a completely different setup. So that made it tricky, we were implementing the feature from scratch.
Then there was the need to allow only three bombs, one of each type. That wasn’t difficult, but there are quite a few places where that decision touched. I’m pretty sure we’re not done with that design, and I expect we’ll decide that it needs to be quite different. We’ll come back to that before we close.
Then there was the weird code saying which column bombs should drop from. We could do something different, but I enjoy the need to duplicate some arbitrary behavior from a legacy program. It tends to put awkward constraints on our work, very similar to what often happens in real life. That has bumped me into an issue that I knew about, the weird order of creation and updating of the invaders. That has left us with an odd situation where the left-most-bottom-most invader turns out to be number 11. We’ll definitely want to fix that.
But it has all stuck together pretty well, and although we definitely ran out of time in the middle of our story, we have the game in a good enough condition to ship, with a bit more capability.
So I’m calling a success. But what about that concern that our design with the three explicit scalar bombs is probably bad. Shouldn’t we have done a lot more design up front so that we won’t have to rework it?
I answer no. If you’ll reflect on pretty much everything I’ve coded in public, my answer is always no. Something running now is better than something well designed now and not running.
My friend and colleague GeePaw Hill writes about “Rework Avoidance Theory”, abbreviated RAT, and its evils. Check out his site and twitter threads for more of his thoughts on the subject.
The basic idea is that we “learn”, somehow, that we’re supposed to avoid rework, and therefore supposed to get the design right at the beginning. My style, and Hill’s, and I think the inherently as nearly right as we can get “agile” style, is to get a good enough design in place, and then change it as needed to make the program better.
Hill writes about “harvesting the value of change”, which is a nice metaphor that he develops well. We’ve all been talking about having a running version from day one, and refactoring the design to avoid technical debt and to accommodate our learning and emerging needs. It’s all the same idea. We learn to evolve the code and design as needed to build the capabilities we’re asked to build, in whatever order we’re asked to build them.
That’s what all this is about. So will I have to change the design for the bombs again? Maybe so. Do I care? Not at all. If I need to do it, I’ll have a better idea. That’s a good thing.
Bye for now, see you next time!