Space Invaders 51
Maybe some tuning. Do we need more start-up settings? Should the test results disappear? What about a FireControl object?
So many little things to do. Most of the hard problems are solved, but there’s this mass of tweaking to do to get the game well tuned. This is the part of projects that I typically hate, but with these games, I feel it less, and I think I know why.
In the olden days, with real product efforts, we were typically well past an externally-imposed arbitrary deadline, constrained not to release the product until a growing list of seemingly unimportant things were accomplished. We couldn’t win. We couldn’t even draw. The game was lost and they made us keep playing. Also we had to walk ten miles to and from work, in 18 inches of snow, uphill both ways.
In these modern agile devops continuous deliver call it what you will times, the product is already out there. People are (we imagine) using it, finding it useful, and telling us how they’d like it improved. We’re happy to improve it with the same feature that would have dragged us down had it been standing in the way of our first release, our first ability to please a user.
Now, with Space Invaders, that’s not quite accurate, because I have somewhat less than one actual user. Dave1707 tries out the program from time to time. As far as I know, he’s the only person who has ever run it. (If you have, it would be a kindness were you to let me know.) But it is running, Dave and I can try it and see what it’s like. And, fair to say, I have these articles, which are releases of the overall “product”, and I know that as many as six people read some of these.
And anyway, it’s something to do.
The upshot is, though, that little things that would have dragged me down in the past, instead now kind of pull me forward. Things that were obstacles to overcome become opportunities to win more.
Hm. There may be something to this idea. Maybe we’ll think about it more. For now, what might we do with Space Invaders?
Something a bit different …
Instead of making a list and then working through it, what if I just pick a thing that might go on the list, and either reject it as something so unimportant it shouldn’t be mentioned, or else do it. Then the next article section is the next thing.
That seems scary: what if I think of something useless?
This whole program is useless. Never mind, doesn’t matter.
Gunner starts in the wrong place
I think the Gunner originally inits where I want it, over to the far left of the screen. But it restarts wherever it died, and there are often bombs falling right there. (That, too, is something we could fix but the rule is we have to fix this one.)
Here’s the init:
function Player:init(pos)
self.pos = pos or vec2(8,32)
self.alive = true
self.count = 0
self.missile = Missile()
self.gunMove = vec2(0,0)
self.ex1 = readImage(asset.playx1)
self.ex2 = readImage(asset.playx2)
self.missileCount = 0
end
8 is far left, although 0 would be further. I’ll try that while we’re here:
function Player:init(pos)
self.pos = pos or vec2(0,32)
...
That’s the initial startup. What happens when he explodes?
function Player:explode()
if Cheat then return end
self.alive = false
self.count = 240
SoundPlayer:play("explosion")
end
Then down in draw
it explodes for a while and then comes back to life:
function Player:draw()
pushMatrix()
pushStyle()
self.missile:draw()
tint(0,255,0)
if self.alive then
sprite(asset.play, self.pos.x, self.pos.y)
else
if self.count > 210 then
local c = self.count//8
if c%2 == 0 then
sprite(self.ex1, self.pos.x, self.pos.y)
else
sprite(self.ex2, self.pos.x, self.pos.y)
end
end
self.count = self.count - 1
if self.count <= 0 then
if Lives > 0 then
Lives = Lives -1
if Lives > 0 then
self.alive = true
end
end
end
end
popStyle()
popMatrix()
end
Well right there where the self.alive = true
is, we could set the position. Since we don’t, the gunner comes back to life right there. But let’s do a bit better. First, I’ll make the change there, however:
...
if Lives > 0 then
Lives = Lives -1
if Lives > 0 then
self.alive = true
self.pos = vec2(0,8)
end
end
...
Now let’s extract those two lines into spawn
:
function Player:spawn()
self.alive = true
self.pos = vec2(0,8)
end
We can nearly use that in init
. We’ll extend it so that we can:
function Player:init(pos)
self.count = 0
self.missile = Missile()
self.gunMove = vec2(0,0)
self.ex1 = readImage(asset.playx1)
self.ex2 = readImage(asset.playx2)
self.missileCount = 0
self:spawn(pos)
end
function Player:spawn(pos)
self.alive = true
self.pos = pos or vec2(0,8)
end
Everything should work and player should now spawn to far left.
Well, to no one’s surprise, that’s wrong. Who typed (0,8) meaning (0,32)?
Easily fixed. With that in place, the game works fine and the player inits to the far left after being killed. With my game-playing skill, that was easy to test manually.
Commit: player inits on left.
Summing up
So there we are. Feature done and shipped. I think the hot water will have recovered from my wife’s morning ablutions, so I’ll go clean up and head out for chai after summing up here.
The magic number (0,32) should be a manifest constant somewhere. Perhaps, had I extracted that first, which I might have done with a copy, I’d have missed the embarrassing (0,8) but we can’t be sure. As always, magic numbers are a concern and we should consider doing something about them. It’s worth noting that most of the magic numbers in the original Space Invaders are stored together in a single place in the listings. But not all of them, although they were perhaps declared with some kind of define
feature even if they were included as small literals in the assembly statements that accept those.
So in that regard, the old Space Invaders is ahead of me. And many other regards, including the number of quarter-dollar coins they accumulated based on their version. Again, I’m doing something wrong.
We extracted the notion of spawning the player, positioning it and bringing it to life. I did that for two reasons: First, I observed that highly nested draw
and realized that it needed improvement. Second, when I put two statements into that if
, it made sense to extract them as a meaningful notion. Third, once extracted, the code nearly duplicated the code in init, so I made it an exact duplicate and then called it.
This is the way.
Along the way, not gonna lie, I though about things to do. Bombs are supposed to pause when the player dies and for two seconds after spawning, if I understand the code. The code in player draw is nested and messy and could be improved. Since we’re working on it, we have a very good right to make it better. Making it better first would have been even, well, better.
And I’ve been thinking about a “FireControl” object that would pull the bomb-dropping logic, and probably the saucer object, out of Army and put it in a separate object. That would simplify Army to just move the invaders around, and hopefully make it easier to make mods to the fire control.
Maybe we’ll work on that next. I haven’t decided and won’t, until I get back.
Hold on, this won’t take long …
What next?
In a stunning reversal of policy, *$ is phasing out straws. Speaking for the world, I approve of this. They offered to provide me with a reusable one, but that strikes me as likely to attract ants. I may have to learn to drink like a grownup.
I think next I’d like to put in a two-second delay of firing. My spec, based on my reading, is that firing should stop when the gunner is not alive, and when it comes to life, firing should resume two seconds later.
We’ve just seen that the gunner / player manages its own restarting. Surely that behavior belongs in the GameRunner. Let’s take a look at that and see where it might reside.
GameRunner creates, the player:
function GameRunner:init()
self.player = Player()
TheArmy = Army(self.player)
self:resetTimeToUpdate()
end
It also draws it (not interesting enough to show here) and updates it every 60th of a second:
function GameRunner:update60ths()
self.time120 = self.time120 + (DeltaTime < 0.016 and 1 or 2)
if self.time120 < 2 then return end
self.time120 = 0
self.player:update()
TheArmy:update()
end
This isn’t terribly well factored but it’s what we have. Now what about that firing thing (well, bombing thing, really)? The Army handles that:
function Army:update()
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)
self.saucer:update(self)
end
function Army:possiblyDropBomb()
if self:canDropBomb() then
self:dropRandomBomb()
end
end
So what if Army had a method or two telling it to hold and release weapons? Then GameRunner could tell the Army when its weapons were free. We’d have to make sure the runner knew the player state but it seems it should be managing that, or at least aware of it anyway.
I really can’t convince myself that this needs to be covered by an automated test, at least for what I’m going to do now. Let’s see what we think as we move forward.
I’m going to add a variable, weaponsFree
to the Army init:
...
self.weaponsFree = false
self.armySize = 55
self.invaderCount = 0
...
And a method to set it:
function Army:setWeaponsFree(aBoolean)
self.weaponsFree = aBoolean
end
At this point, the game should no longer drop alien bombs.
Oops, forgot to check the flag.
function Army:canDropBomb()
return self.weaponsFree and self.bombCycle > self.bombDropCycleLimit
end
Now, as I was saying, the game should no longer drop bombs. And I’m right this time. Also a test fails:
16: bombs have delay between firing -- Actual: false, Expected: true
_: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)
Sweet. We need to free the weapons for this test to run. A bitty little test for a bitty little method:
_:test("bombs have delay between firing", function()
local enough = 100
local theArmy = Army()
theArmy:setWeaponsFree(true)
_:expect(theArmy:canDropBomb()).is(false)
theArmy:updateBombCycle(enough)
_:expect(theArmy:canDropBomb()).is(true)
end)
I don’t really like that method with its boolean parameter. Let’s keep that in mind.
Tests run.
No, let’s fix it. Two methods, weaponsFree
and weaponsHold
. We’ll change the flag name to free up the name.
function Army:weaponsFree()
self.weaponsAreFree = true
end
function Army:weaponsHold()
self.weaponsAreFree = false
end
With the other necessary changes:
function Army:canDropBomb()
return self.weaponsAreFree and self.bombCycle > self.bombDropCycleLimit
end
_:test("bombs have delay between firing", function()
local enough = 100
local theArmy = Army()
theArmy:weaponsFree()
_:expect(theArmy:canDropBomb()).is(false)
theArmy:updateBombCycle(enough)
_:expect(theArmy:canDropBomb()).is(true)
end)
Tests run, no firing. I like this game. Now let’s init the player to not be alive, and we’ll send him a spawn when we want to start the game from GameRunner:
function Player:init(pos)
self.count = 0
self.missile = Missile()
self.gunMove = vec2(0,0)
self.ex1 = readImage(asset.playx1)
self.ex2 = readImage(asset.playx2)
self.missileCount = 0
self.alive = false
end
At this point the player should never appear, and no bombs should drop. (The invaders should still march, however.)
I am surprised to find that a gunner does appear, right away. Is someone triggering him, or did my test do that? Ah. The draw
function does it:
function Player:draw()
pushMatrix()
pushStyle()
self.missile:draw()
tint(0,255,0)
if self.alive then
sprite(asset.play, self.pos.x, self.pos.y)
else
if self.count > 210 then
local c = self.count//8
if c%2 == 0 then
sprite(self.ex1, self.pos.x, self.pos.y)
else
sprite(self.ex2, self.pos.x, self.pos.y)
end
end
self.count = self.count - 1
if self.count <= 0 then
if Lives > 0 then
Lives = Lives -1
if Lives > 0 then
self:spawn()
end
end
end
end
popStyle()
popMatrix()
end
We just implemented that an hour ago. Let’s change that to call back to GameRunner to request spawning. We’ll leave it up to GameRunner to count lives as well. Might be an opportunity to move that inside GameRunner.
...
self.count = self.count - 1
if self.count <= 0 then
Runner:requestSpawn()
end
...
For now, we’ll just do what that code used to do:
function GameRunner:requestSpawn()
if Lives > 0 then
Lives = Lives - 1
self.player:spawn()
end
end
Now I expect the player to spawn immediately. Some tests fail, let’s check them.
24: Player counts shots fired -- Actual: 0, Expected: 1
9: Bomb hits gunner -- Actual: false, Expected: true
_:test("Bomb hits gunner", function()
Runner = GameRunner()
local Gunner = Runner.player
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
Gunner.alive = true
_:expect(bomb:killsGunner()).is(true)
bomb.pos.x = 48 -- covers x = 48,49,50
Gunner.alive = true
_:expect(bomb:killsGunner()).is(true)
bomb.pos.x = 47 -- 47,48,49
Gunner.alive = true
_:expect(bomb:killsGunner()).is(false)
bomb.pos.x = 65 -- 65,66,67
Gunner.alive = true
_:expect(bomb:killsGunner()).is(true)
bomb.pos.x = 66 -- 66,67,68
Gunner.alive = true
_:expect(bomb:killsGunner()).is(false)
end)
Now this, boys and girls, is exactly why we don’t like tests with multiple assertions. Which one of these failed? It appears that one assertion failed and then six passed. So it must be the first one. (We did put an optional message in for just this situation and it may come to that. But let’s look and see what’s up.
Interestingly the six tests at the end check both hits and non-hits. What’s special about the first one?
Ah. We’re initializing the player to alive = false. We just need to set it before the first test, as we do with the others. Yes, that works. Now for #24:
_:test("Player counts shots fired", function()
local Gunner = Player()
_:expect(Gunner:shotsFired()).is(0)
Gunner:fireMissile()
_:expect(Gunner:shotsFired()).is(1)
end)
I’ll bet this is an alive thing also. Where’s fireMissile
?
function Player:fireMissile()
if not self.alive then return end
if self.missile.v == 0 then
self.missile.pos = self.pos + vec2(7,5)
self.missile.v = 1
self.missileCount = self.missileCount + 1
SoundPlayer:play("shoot")
end
end
Yep, same fix.
_:test("Player counts shots fired", function()
local Gunner = Player()
Gunner.alive = true
_:expect(Gunner:shotsFired()).is(0)
Gunner:fireMissile()
_:expect(Gunner:shotsFired()).is(1)
end)
Huh, here’s a surprise:
24: Player counts shots fired -- Player:93: bad argument #-2 to '__add' (vec2)
function Player:fireMissile()
if not self.alive then return end
if self.missile.v == 0 then
self.missile.pos = self.pos + vec2(7,5)
self.missile.v = 1
self.missileCount = self.missileCount + 1
SoundPlayer:play("shoot")
end
end
Ah, The pos
isn’t initialized until spawn. Let’s actually change the test to call spawn, that’s less invasive anyway:
_:test("Player counts shots fired", function()
local Gunner = Player()
Gunner:spawn()
_:expect(Gunner:shotsFired()).is(0)
Gunner:fireMissile()
_:expect(Gunner:shotsFired()).is(1)
end)
There’s a bit of a hidden problem here. I was taught, by Kent Beck, the notion of “complete creation method”, which means that when we create an object, it should be ready to go, not requiring any further initialization. That doesn’t mean it should necessarily actually start operating, but it should be complete, with no holes in it. In that spirit, I’ll initialize pos
in Player, even though it’s redundant. That leaves this:
function Player:init(pos)
self.pos = pos or vec2(0,32)
self.count = 0
self.missile = Missile()
self.gunMove = vec2(0,0)
self.ex1 = readImage(asset.playx1)
self.ex2 = readImage(asset.playx2)
self.missileCount = 0
self.alive = false
end
function Player:spawn(pos)
self.alive = true
self.pos = pos or vec2(0,32)
end
There’s duplication of that magic number now Let’s fix that:
function Player:init(pos)
self:setPos(pos)
self.count = 0
self.missile = Missile()
self.gunMove = vec2(0,0)
self.ex1 = readImage(asset.playx1)
self.ex2 = readImage(asset.playx2)
self.missileCount = 0
self.alive = false
end
function Player:spawn(pos)
self.alive = true
self:setPos(pos)
end
function Player:setPos(pos)
self.pos = pos or vec2(0,32)
end
It’s still magic, but it’s better isolated to one location.
This odd little refactoring seems to me to foreshadow another one, something about explicit setting of the alive
flag, compared to our having methods to control whether weapons are free. We’ll see about that.
Right now, the game isn’t playable because the invaders don’t drop bombs, but we have improved the structure a bit, in preparation for the timing delay on the firing.
Now that spawning is requested, maybe we can put some timing in the GameRunner.
function GameRunner:requestSpawn()
if Lives > 0 then
Lives = Lives - 1
self.player:spawn()
self:setWeaponsDelay()
end
end
Hm, that looks like it would be useful.
function GameRunner:setWeaponsDelay()
self.weaponsTime = ElapsedTime + 2
end
Well, now weaponsTime is two seconds after we spawn a gunner. What if we check that in update:
function GameRunner:update60ths()
self.time120 = self.time120 + (DeltaTime < 0.016 and 1 or 2)
if self.time120 < 2 then return end
self.time120 = 0
self.player:update()
TheArmy:update()
if self.weaponsTime and ElapsedTime >= self.weaponsTime then
self.player:weaponsFree()
self.weaponsTime = 0
end
end
I think this will free weapons 2 seconds after the first spawn. After that I’m hot sure what happens. Silly me, that’s an Army method:
function GameRunner:update60ths()
self.time120 = self.time120 + (DeltaTime < 0.016 and 1 or 2)
if self.time120 < 2 then return end
self.time120 = 0
self.player:update()
TheArmy:update()
if self.weaponsTime > 0 and ElapsedTime >= self.weaponsTime then
TheArmy:weaponsFree()
self.weaponsTime = 0
end
end
OK, as I kind of feared, the bombs keep falling after the first kill, but they do wait two seconds before they start at the beginning of the game.
We have green tests and a bit of new function. Commit: two second bombing delay on game start (only).
Ah, we do have a bug, though. I mean defect. The way we’re hooked up now, Lives gets initialized to 3, and the player to not alive, so the draw function requests a new player, counting down immediately to 2, so we only get two players.
Main tab presently initializes Lives, and when Lives is zero, shows Game Over. That capability really belongs to GameRunner, doesn’t it? We’ve been moving capability around as we encounter it, so it’s no surprise that not everything is in quite the right spot yet.
It’s going to take a bit of design thinking to make the next changes, however, so we’ll “fix” this bug by initiaizing Lives to 4 and move on to lunch with a working, very slightly improved program.
Progress is progress. I’ll sum up after lunch.
I just invented a thing:
_:ignore("Lives hack initialized to 4", function()
end)
This empty test turns the test display yellow and says 1 ignored. That should remind me of where I was when next I show up to program.
Game plays correctly. Commit: hack fix for 3 lives.
Off to lunch.
Later
Well, I’ve finished lunch, a bit of reading, and if you must know, a bit of a siesta as well. And I have an idea: when the gunner dies, it should call for a weapons hold.
function Player:explode()
if Cheat then return end
self.alive = false
self.count = 240
SoundPlayer:play("explosion")
end
This becomes …
function Player:explode()
if Cheat then return end
Runner:playerExploding()
self.alive = false
self.count = 240
SoundPlayer:play("explosion")
end
function GameRunner:playerExploding()
TheArmy:weaponsHold()
end
That does the trick nicely. Now the bombs stop falling when the player explodes, and start two seconds after it respawns.
Commit: two second weapons hold after every respawn.
This was a good idea. Still past working hours, let’s sum up.
Summing Up
No real planning
Today I tried just working on the first thing that came to mind, while reserving the right to decline if something seemed too hard or otherwise not a good idea. Why? Well, because maybe making a list of everything we might do is too much planning? Probably not. I just thought I’d see if I could spend less time warming up to do something. I do think I’d benefit from a list of things needing doing, and tomorrow I’ll do one. Or not.
Tiny methods
You probably have some kind of reaction to all the tiny methods we’ve been generating lately. Here are a few of them:
function GameRunner:playerExploding()
TheArmy:weaponsHold()
end
function GameRunner:setWeaponsDelay()
self.weaponsTime = ElapsedTime + 2
end
function GameRunner:resetTimeToUpdate()
self.time120 = 0
end
function GameRunner:update()
self:updateAlways()
self:update60ths()
end
function Army:adjustTiming(score)
self.bombDropCycleLimit = self:timingValue(score)
end
function Army:deleteBomb(aBomb)
aBomb.alive = false
end
function Army:reportingForDuty()
self.invaderCount = self.invaderCount + 1
end
function Army:directKillInvader(invaderNumber)
self.invaders[invaderNumber].alive = false
end
function Army:saucerTop()
return 205
end
function Army:setPosition(pos)
self.invaders[1].pos = pos
end
function Army:xPosition()
return self.invaders[1].pos.x
end
function Army:yPosition()
return self.invaders[1].pos.y
end
function Invader:shieldHitAdjustment()
return vec2(0,0)
end
function Invader:explosionBits()
return self.sprites[self.picture+1]
end
function Invader:bitSize()
return 16, 8
end
function Bomb:explosionBits()
return BombExplosion
end
function Bomb:shieldHitAdjustment()
return vec2(2,1)
end
function Bomb:bitSize()
return 3, 4
end
function Missile:explosionBits()
return self.explosion
end
function Missile:shieldHitAdjustment()
return vec2(2,-1)
end
function Missile:explosionColor()
return self.pos.y > TheArmy:saucerTop() and color(255,0,0) or color(255)
end
function Missile:bitSize()
return 1, 4
end
That’s not even all of them. Many of them return constants. In another language those might be represented by a define
of some kind, but Lua doesn’t have that. Others among them contain a bit of code that I felt needed a name, for example:
function Missile:explosionColor()
return self.pos.y > TheArmy:saucerTop() and color(255,0,0) or color(255)
end
This one-liner just decides whether the missile explosion will be white or red.
I’m used to this kind of thing, and don’t look into the little methods unless I actually wonder how they do what they do. I might also look into one if its meaning wasn’t clear, but when I did, my next step would be to give it a more meaningful name and leave it as a one-liner.
YMMV. This is very much a Smalltalk thing, and we don’t usually see it in most Java, or even most Ruby, though we do see it in the hands of many experienced OO people. I don’t have the luxury of reading much functional-style programming, but there too I’d expect to see small functions, though often they’d be ones covering a map, select, or reduce.
Again, YMMV. For me, I like this, and it makes the code easier for me to work with.
General observations
Today’s features went in fairly nicely. I would argue that I should have realized sooner that I could get the two-second delay so easily on other than the first gunner / player. That I didn’t speaks to a few things, including that I was hungry and tired of coding and writing, but it also suggests that the interaction between GameRunner, Player, and Army isn’t quite right. This may argue for a WeaponsMaster or FireControl object, or maybe just for a better alignment of responsibility.
I noticed the global TheArmy
and think something should be done about that. It would seem that the GameRunner knows the Army and the separate global should be unnecessary. A quick scan of the code confirms that. We’ll try to remember to smooth that out soon.
Overall, we got two new capabilities, improving the gunner / player a bit, and we improved the code a bit as well.
A good morning, even if a few minutes of it went into the afternoon.
See you next time!