Space Invaders 17
My plan for the day is to retrain myself by reading this code that I’ve not touched for two whole days, and then to see about moving things forward. My guess: invader missiles.
Just as I like to close each programming session with a bit of looking back, I like to open each one with, first, a bit more looking back, in the light of a new day, and then a bit of looking forward, setting a rough goal for the session. I find it best to hold on loosely to these goals, because things happen during every session, influencing where we go and how we travel.
We are invariably in unknown territory when we program. Yes, much of the territory is familiar – we just wrote it, after all – but the territory we’re about to enter is unknown, because we haven’t written it yet. We may think we know just how it’s going to go, but in my long experience, we’re wrong as often as we’re right. And even to hit that ratio, we probably need to conveniently forget a lot of missteps and mistakes.
So I expect to move toward invader missiles soon, maybe today. But first we need to look at the code.
I do that by scanning whatever seems interesting, and if I see something interesting, I explore it a bit more.
Right off the bat, we see this in setup
:
function setup()
runTests()
--if true then return end
TheArmy = Army()
setupGunner()
Missile = {v=0, p=vec2(0,0)}
invaderNumber = 1
end
See that comment after runTests
? I put that there when I was writing a test yesterday. There’s another one in draw
, which we may look at later. Those two lines keep the game from running, which is very helpful when you’re running the tests.
I’m not sure it’s really needed, though. Setup won’t complete until after the tests have run, and drawing won’t start until setup is complete. But I still feel that I’ve run into issues that have made me want to keep the system from starting.
I’m probably wrong. I’ll make a mental note to understand why I want to stop the system, next time I feel that way, and I’ll remove these lines.
Also, note the creation of a single Missile. This is just a kid of placeholder for the gunner’s real missiles, which will be a collection, since more than one can be flying toward the invaders at a time during real gameplay. I imagine that what we’ll do when the time comes is provide a collection of missiles, like the collection of invaders, and process them similarly. We’ll have gunner missiles and invader ones, and they look differently and behave differently, so there’s some discovering to be done there.
My hope today is to work on the invader ones a bit.
Reading on … looking at 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)
TheArmy:draw()
drawGunner()
drawMissile()
popStyle()
popMatrix()
TheArmy:update()
end
I’m reminded that I’ve been flinging lots of pushing and popping of style and matrix. I don’t think I’ve mentioned it, but I’m kind of trying to develop the habit of doing that any time I set out to draw some conceptual entity. If all you do is draw,, the push and pop aren’t needed, but you do need them if you use translate, rotate, or scale, or when you change colors.
My guess is that in this app, using them is excessive. But as a rule, they’re valuable, and I’m developing the habit of using them, to see if I like that habit.
I’m also reminded that as the game is right now, the boundaries of the game screen (224x256) are not visible, and I wish they were. It would help me see whether invaders are moving correctly and so on.
Last time I tried to do that, the game area was clearly off center. I think, just to write a line of code, I’ll add that in right now.
Note that translate:
translate((1366-1024)/8,0)
That sets the new 0,0 point of the game somewhere. But why there? What are those very magic numbers meant to be? I’m not entirely sure any more, but I know they are wrong.
If they were right, I could draw a 224x256 rectangle and it would encompass the game screen, centered on the real screen (because that’s how I want it). But that’s not the case. I put this in:
stroke(255)
fill(255)
scale(4) -- makes the screen 1366/4 x 1024/4
translate((1366-1024)/8,0)
fill(0)
strokeWidth(1)
rect(0,0, 224,256)
fill(255)
And here’s the screen:
Whatever else you may say for it, that’s definitely not centered. The bug is the translate
. I was going to draw a picture but let’s see if we can do without.
Despite that the rules of my articles include “warts and all”, I flat refuse to show you how confused in mental arithmetic I got before sorting this trivial thing out. A picture would have helped. An outing for chai helped more. I reasoned as follows:
I want to be 112 scaled pixels in from center. So if I translated to WIDTH/2 before scaling, and then translated by -112 after, that would be where I wanted to be. Scaling to WIDTH/2 before scaling is the same as translating to WIDTH/8 after scaling by 4. I can do it in one step. Therefore:
scale(4) -- makes the screen 1366/4 x 1024/4
translate(WIDTH/8-112,0)
So that’s nice. Commit: centered, finally.
Two lessons here.
First, with a partner programming with me, this would have taken no time because either my pair would just see the answer or we’d look at each other stupidly and do the math on a card.
Second, this is why I use scaling and translation so freely in my graphical programs. It means that I don’t have to do it with in-line math, at which I apparently do not excel.
OK, where were we?
Invader missiles. Having wasted so much time on that silly centering thing, I’m hot to do something useful. (Despite indications that I’d be wise to back away slowly and take up an easier line of work.)
Let’s think about the invader missiles a bit.
Invader Missiles
We know there are a few different kinds of missiles, but they’re mostly just graphically different. I think there are three kinds. Two drop down randomly, and one, if I recall the history, is aimed. I take that to mean that the aimed one is dropped down from right above the gunner. None of them move other than straight down as far as I know.
It’s clear that only an invader who is at the bottom of her column can drop a missile. Otherwise she’d blow up one of her sisters, and that would be bad.
Presumably there is some random function that decides, for each eligible invader, whether she wants to fire a missile. Then we’ll randomly decide other details, later.
I’ve not reviewed the original Space Invaders to get its numbers, but I expect the details won’t matter. Watching a gameplay video, it seems that the invaders start out with only one missile on the screen at a time, but as the game goes on I’ve seen two or three flying at once. (It also looks as if the gunner can only have one missile in flight at a time.) I’ll have to review the original game code to suss out the details but for now they don’t matter.
Let’s start by just giving each invader a chance to drop a missile and we’ll tune details from there.
I’m wondering when the invader should get this opportunity. We move them one at a time. Let’s try having them fire before they move, if they’re going to fire at all.
We have this update code:
function Army:doUpdate()
local continue = true
while(continue) do
continue = self:nextInvader():update(self.motion, self)
end
end
This code updates exactly one live invader, because their update code is this:
function Invader:update(motion, army)
if self.alive then
self.pos = self.pos + motion
if self:overEdge() then
army:invaderOverEdge(self)
end
return false
else
return true
end
end
What if we just add in a fireMissile
call here:
function Invader:update(motion, army)
if self.alive then
self:fireMissile() -- <---
self.pos = self.pos + motion
if self:overEdge() then
army:invaderOverEdge(self)
end
return false
else
return true
end
end
The code for that should look roughly like this:
function Invader:fireMissile()
if not self:isBottom() then return end
if math.random() < 0.99 then return end
self:hmmThisShouldBeFireMissile()
end
I got to the end there and realized I want the actual firing of the missile to be named fireMissile
. I’m also concerned about the word “fire” and the notion of “missile”. I think we’ll call the invader ones bomb, and drop them, just to keep the ideas separate from gunner missiles. So, renaming a bit …
function Invader:update(motion, army)
if self.alive then
self:possiblyDropBomb()
self.pos = self.pos + motion
if self:overEdge() then
army:invaderOverEdge(self)
end
return false
else
return true
end
end
function Invader:possiblyDropBomb()
if not self:isBottom() then return end
if math.random() < 0.99 then return end
self:dropBomb()
end
I think this expresses our idea fairly well. I’ll stub isBottom
to true, and build something for dropBomb
.
function Invader:isBottom()
return true
end
I realize at this point that isBottom
will need to know the army one way or another, to check her siblings, so I’ll pass that in while I’m thinking of it:
function Invader:update(motion, army)
if self.alive then
self:possiblyDropBomb(army)
self.pos = self.pos + motion
if self:overEdge() then
army:invaderOverEdge(self)
end
return false
else
return true
end
end
function Invader:possiblyDropBomb(army)
if not self:isBottom(army) then return end
if math.random() < 0.99 then return end
self:dropBomb()
end
To do the drop, we’ll need at least one bomb, and I’m inclined to do the collection right now. I’ll start using it here, and let using it tell me more about what I want it to do.
This time I think I’ll build a class right away, because I need somewhere to stand.
OK, I just went crazy here and wrote all this:
Bomb = class()
function Bomb:init(pos)
self.pos = pos
end
function Bomb:draw()
pushStyle()
fill(255)
stroke(255)
strokeWidth(255)
rect(self.pos.x,self.pos.y, 1,4)
popStyle()
end
function Bomb:update()
self.pos.y = self.pos.y - 2
end
If I were to manage to create some of these, something might happen. Then:
function Invader:dropBomb()
local bomb = Bomb(self.pos - vec2(0,16))
Bombs[bomb] = bomb
end
I’m positing a table named Bombs that is indexed by Bomb id, pointing to the bomb, much as we did in the asteroids program. That table needs to be created and processed in Main:
function draw()
pushMatrix()
pushStyle()
background(40, 40, 50)
showTests()
stroke(255)
fill(255)
scale(4) -- makes the screen 1366/4 x 1024/4
translate(WIDTH/8-112,0)
fill(0)
strokeWidth(1)
rect(0,0, 224,256)
fill(255)
TheArmy:draw()
drawBombs()
drawGunner()
drawMissile()
popStyle()
popMatrix()
TheArmy:update()
end
function drawBombs()
for b,bomb in pairs(Bombs) do
b:update()
b:draw()
end
end
So that nearly works. The bombs are so narrow as to be invisible, and they move very rapidly. I’ll make them wider and slower and then make a video. I recall that the real missiles are three pixels wide, so for now I do this:
function Bomb:draw()
pushStyle()
fill(255)
stroke(255)
strokeWidth(255)
rect(self.pos.x,self.pos.y, 3,4)
popStyle()
end
function Bomb:update()
self.pos.y = self.pos.y - 1
end
And it looks like this:
So that’s nearly good. I’ll commit this: initial bombs, and then reflect.
Reflection
This was highly speculative. I wrote 40 lines of code without a test, either automated or just by running the system. It really rather surprises me that it worked without a fuss.
One reason that it worked was sheer luck. I would guess that I inject at least one bug per 50 or 100 lines of code, and maybe more. So my internal random mistake maker got lucky.
Another reason that it worked, however, was a bit more in my favor. I programmed “by intention”, writing small methods and naming them by my intention for what they had to do. For example, I called drawBombs
and then wrote it:
function drawBombs()
for b,bomb in pairs(Bombs) do
b:update()
b:draw()
end
end
That practice gives me an automatic break between “what am i trying to do” and “how shall I do that”. To me, that seems to create two little pauses of thought, the what and the how. At the same time, breaking out each idea into its own function makes it easier to quickly scan the code and see whether it looks right.
A third advantage was that I had used this trick for storing things in the Asteroids game, so it was close to the top of my bag of tricks.
So this went rather well, and I am frankly a bit surprised that there wasn’t at least a bit of debugging.
So it “works”. Is it “right”? Surely not, but is it even right for what it does? I think not.
I believe that Bombs should belong to the Army, not to Main. One reason is that the invaders have access to Army, and therefore they can arrange to have their bombs managed more readily.
Let’s see what it would take to do that. Here, an invader drops a bomb:
function Invader:dropBomb()
local bomb = Bomb(self.pos - vec2(0,16))
Bombs[bomb] = bomb
end
She knows the global bomb. Instead, let’s pass in the Army, which we have in the method that calls this, and have her give the bomb to the Army to tuck away.
function Invader:possiblyDropBomb(army)
if not self:isBottom(army) then return end
if math.random() < 0.99 then return end
self:dropBomb(army)
end
function Invader:dropBomb(army)
army:dropBomb(Bomb(self.pos - vec2(0,16)))
end
So then in Army:
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, 1, Invader(p))
end
end
self.invaderNumber = 1
self.overTheEdge = false
self.motion = vec2(2,0)
self.updateDelay = 1/65 -- less than 1/60th by a bit
self:resetTimeToUpdate()
self.bombs = {}
end
function Army:dropBomb(bomb)
self.bombs[bomb] = bomb
end
And in draw:
function Army:draw()
pushMatrix()
pushStyle()
rectMode(CENTER)
for i,invader in ipairs(self.invaders) do
invader:draw()
end
for b,bomb in pairs(self.bombs) do
b:update()
b:draw()
end
end
And of course I removed the Bomb stuff from Main.
Full disclosure: I forgot at first to say self.bombs
in the loop, so it crashed on a reference to Bombs
. Fortunately I had remembered to remove the init. Otherwise it would have run but no bombs would have appeared, confusing me for ages no doubt.
Now I expect it to work. Wonder why I’m wrong. Strangely enough, I’m not wrong, it does work. Commit: move bombs into Army.
Now I’d like to kill the bombs if their y coordinate goes negative:
function Bomb:update()
self.pos.y = self.pos.y - 1
end
This’ll need Army as a parameter and to check its coordinate:
function Bomb:update(army)
self.pos.y = self.pos.y - 1
if self.pos.y < 0 then
army:deleteBomb(self)
end
end
And …
function Army:draw()
pushMatrix()
pushStyle()
rectMode(CENTER)
for i,invader in ipairs(self.invaders) do
invader:draw()
end
for b,bomb in pairs(self.bombs) do
b:update(self)
b:draw()
end
end
I decided not to implement deleteBomb
, to see it break. And it does:
Bomb:19: attempt to call a nil value (method 'deleteBomb')
stack traceback:
Bomb:19: in method 'update'
Army:72: in method 'draw'
Main:25: in function 'draw'
Perfect. Therefore:
function Army:deleteBomb(bomb)
self.bombs[bomb] = nil
end
This works as far as I can tell, but of course I don’t know that the bombs aren’t flying downward forever. I’m sure from the code that they are being destroyed but I can’t see it in the game.
Perhaps a test would be a good idea.
_:test("bombs get deleted", function()
local army = Army()
local bomb = Bomb(vec2(122,10))
army:dropBomb(bomb)
_:expect(#army.bombs).is(1)
for i = 1,9 do
bomb:update(army)
end
_:expect(#army.bombs).is(1)
bomb:update(army)
_:expect(#army.bombs).is(0)
end)
This works after remembering that it is dropBomb
, not addBomb
. So we move our bomb 9 times and he’s good, one more time and he’s not. This test is tightly coupled to the bomb step being 1, so it will surely fail later on
This could be a sign to provide a parameter to the bomb update. We could do that right now and that would prepare us for the future, when we will surely change the update again, and protect the test against future change as well. Let’s try that:
_:test("bombs get deleted", function()
local army = Army()
local bomb = Bomb(vec2(122,10))
army:dropBomb(bomb)
_:expect(#army.bombs).is(1)
for i = 1,9 do
bomb:update(1,army)
end
_:expect(#army.bombs).is(1)
bomb:update(1,army)
_:expect(#army.bombs).is(0)
end)
No, I want the amount to be after army:
_:test("bombs get deleted", function()
local army = Army()
local bomb = Bomb(vec2(122,10))
army:dropBomb(bomb)
_:expect(#army.bombs).is(1)
for i = 1,9 do
bomb:update(army,1)
end
_:expect(#army.bombs).is(1)
bomb:update(army,1)
_:expect(#army.bombs).is(0)
end)
Why? Because I think that parameter is more likely to change than the army one, and because I can now default it in the update:
function Bomb:update(army, increment)
self.pos.y = self.pos.y - (increment or 1)
if self.pos.y < 0 then
army:deleteBomb(self)
end
end
Uh oh, I didn’t notice that the test didn’t run: I thought it had. I need to flash the screen or something. It’s getting zero back after the 1-9 loop. Why?
I changed the test to this:
_:test("bombs get deleted", function()
local army = Army()
local bomb = Bomb(vec2(122,10))
army:dropBomb(bomb)
_:expect(#army.bombs).is(1)
for i = 1,9 do
bomb:update(army,1)
print(i, bomb.pos.y)
_:expect(#army.bombs).is(1)
end
bomb:update(army,1)
_:expect(#army.bombs).is(0)
end)
The print shows pos.y ticking down 9 8 7 etc. But the bombs table shows zero entries always. Why? Because, Jeffries you tiny fool, # doesn’t work on hashed value tables, only on array tables.
Well! OK …
function countTable(t)
local c = 0
for k,v in pairs(t) do
c = c + 1
end
return c
end
_:test("bombs get deleted", function()
local army = Army()
local bomb = Bomb(vec2(122,10))
army:dropBomb(bomb)
_:expect(countTable(army.bombs)).is(1)
for i = 1,9 do
bomb:update(army,1)
_:expect(countTable(army.bombs)).is(1)
end
bomb:update(army,1)
_:expect(countTable(army.bombs)).is(0)
end)
Fails again on the last call, saying actual 1 expected 0. Why? Because the check in bomb
is for < 0
. I think that’s correct and the test is wrong.
_:test("bombs get deleted", function()
local army = Army()
local bomb = Bomb(vec2(122,10))
army:dropBomb(bomb)
_:expect(countTable(army.bombs)).is(1)
for i = 1,10 do
bomb:update(army,1)
_:expect(countTable(army.bombs)).is(1)
end
bomb:update(army,1)
_:expect(countTable(army.bombs)).is(0)
end)
That works. Commit: test for bomb deletion.
Well, I have some kind of video interview call later today, so I think we’ll wrap this here. We do have bombs falling, and that is, at least in the context of the game, a good thing.
Let’s sum up:
Summing Up
Best things first, popping in the first cut at invader bombs went very smoothly. We expressed our intention – what we were trying to do – and then unwound those intention names into further intentions, until at the bottom we were able to write a few lines of code that did the thing.
That’s really how it’s supposed to work.
We worked entirely without tests, and while that’s not something I’m proud of, it is certainly something that seems quite common when I’m programming visual things. I did manage to inject a test to make sure the bombs got deleted when they went off-screen, and that took almost as long as actually doing the whole job. Most of that was due to my writing #army.bombs
, expecting to get the table count, when in fact one does not get the table count. I’ve made that mistake before. I’ll try not to make it again, but no promises.
I began the morning with a mental arithmetic exercise that was even more depressing than counting down from 100 by sevens. Then the answer suddenly became obvious. What should be obvious is that I’m terrible at mental arithmetic, and that has been obvious to me since the 1940s. I hated rote arithmetic practice, wouldn’t do it, and learned to count on virtual fingers incredibly rapidly. That technique didn’t extend to scaling and translation.
Anyway, we have a place to stand for bombs now. Before us we have lots more to do, including:
- Make the bombs and other objects look right. (This includes animation.)
- Put the shields in place
- Incrementally destroy shields with bombs.
- Destroy gunner with bombs.
And much much more, including working out all the timing issues to make it look like the original game.
I am inclined to do the gunner destruction next, but there are reasons why we might not. We’ll talk about that next time.
See you then!