Space Invaders 24
If the world isn’t flat, how come these invaders keep falling off the edge?
For the work portion of this session, I’ll see about adjusting invader motion so that they don’t fall off the edge. To be completely clear, they don’t actually fall, though that would be kind of fun. But they do go over the edge:
On the left, they go right up to the edge, but not over.
Recall that there is a bit of trickiness with the edge handling, because if all the invaders in the rightmost column are dead, the rows do not reverse until a live alien gets to the edge.
From yesterday we recall that invaders are 16 pixels wide, and they have at least two pixels blank on each side. I think what I’d like to do is have them ensure that none of their pixels, even the blank ones, ever goes over.
The screen coordinates are 0 on the left and 223 on the right. And here’s our current code:
function Invader:overEdge()
return self.pos.x >= ScreenRight or self.pos.x <= ScreenLeft
end
Now when I started thinking about this, I was thinking about adding the invader width to the right side check and so on. Silly me. We can adjust ScreenRight
and ScreenLeft
to get any effect we want. Here they are now:
Invader = class()
local ScreenRight = 224
local ScreenLeft = 0
As I’ve mentioned before, the local
there really has no useful effect because we’re in the outer scope, so they are really global. I’m not going to worry about that right now. So first of all that 224 should have been 223. And since our invaders are 16 wide, it should be 223-16, which is, um, 207. I’ll set that and see how it looks. I have the feeling that I’d like a bit more margin on each side. No way to test that but to look, I guess.
That looks OK, I think, and when I turn off the rim around the screen that’s part of my grid, it should be just fine.
So that was easy. Commit: invaders stay on screen.
Now What?
That was the extent of my commitment this morning. What shall we do now?
One thing is that it appears in the original source that the invaders step down by at least 8 pixels, a whole rank-height at a time. We have this:
local reverse = -1
local stepDown = vec2(0,-2)
function Army:adjustMotion()
if self.overTheEdge then
self.overTheEdge = false
self.motion = self.motion*reverse + stepDown
else
self.motion.y = 0 -- stop the down-stepping
end
end
Let’s change that -2 to -8, so they’ll approach more quickly.
Oh yes, far more menacing. Commit: invaders step down 8.
And Now What??
Making these two important behavior changes by adjusting a couple of constants pleases me, and it troubles me.
It pleases me, because there were constants to adjust. Too often we’ll find random “magic numbers” sprinkled about in the code. Sometimes it will be clear what the numbers mean and sometimes not, but they certainly don’t just sing out “clarity!”. At least these values are named, and that’s good.
What doesn’t please me is that the first two named constants were at the top of the Invader
class, and the two we just looked at are in the middle of the Army
class. Now those are the right classes to be associated with. The use of those numbers is only in the associated class. But I don’t like that they were at the top in the one case and in the middle in the other.
What’s challenging, though, is that the second case, the numbers were right next to the only function that uses them. So that’s convenient, it’s all together. But they’re hard to find.
Commonly, we might choose to put all our constants in a single place. In Asteroids, we put a lot of those values in the Universe
class, if I recall. We also had a group inside Universe
for sounds, and we considered whether there should be a member collection for the various constants.
I wouldn’t swear that every constant in Asteroids is in Universe, or even in whatever class should best hold it, but I did try to keep things organized.
Things here are getting less organized, and not just in the arrangement of constants. The Main tab manages all kinds of things, like the Missile and its firing, the Gunner, and I just found a function drawInvader
that isn’t used at all.
Commit: delete unused drawInvader.
I think we should do some cleanup … I mean … we should reduce some of our technical debt.
But first, a run for chai. 0838 - 0905. Caught the light at M-36. Life is good.
Let’s move on …
Remarks on Testing
My good friend @GeePawHill has started a series of short videos that you should check out. Here’s the first.
In this video he starts a fun little game project and quickly moves to the organization he needs for testability. Definitely worth checking out.
In the Space Invaders series, in the preceding Asteroids one, and in almost all my Codea work, I’ve found it very difficult to come up with reasonable microtests, despite the fact that I really prefer to work with them. The reason is that graphical things are hard to test with automated tests. It’s easy to see whether the Invaders are over the edge, but it’s not easy to test whether they are, because if you just test your comparison against 224, it will of course work, but the sweet little invader babes are still going over the edge.
To make things testable, we try to separate the graphics from the logic as much as possible. As you’ll see in GeePaw’s series, the usual breakout is called “Model-View”. For my work with Codea, it has been difficult to make that breakout, because almost all the logic is about the graphics.
For an example, take the bomb-shield collision logic that we’ll work on soon. When a bomb hits the shield, we’ll do something that will “blow up” part of the shield. That will be done by erasing some of the bits in the shield’s bitmap. When next we go to decide if a bomb has hit that shield, it won’t be enough that it’s inside the rectangle. We’ll want to check whether the bomb has hit at least one non-zero pixel. If it hasn’t, then it hasn’t hit the shield, and it proceeds. If it has hit a non-zero pixel, then we blow up some more shield.
I have a fairly clear notion of how to do this. If the bomb has overlap with the rectangle of the shield, I’ll figure out where in the shield it is, and test all the bits it would cover to see if any are non-zero.
Since we can fetch the bits in a bitmap with image:get
, and we can even copy a sub-rectangle with image:copy
, this should be “easy”.
What about testing it? The obvious mistake in doing this will be selecting the wrong bits to check in the bitmap. So I can draw an example on paper (or Paper™, more likely), compute the values by hand, and write a test, giving it rectangles that are like those in the game.
In fact, I promise that I will do that when the time comes. Even that test feels pretty silly to me, because I can almost certainly get it right by counting on my fingers, and it’ll be pretty clear on the screen if it doesn’t work. But what about the checking for a non-zero bit? How hard can it be to add up the pixel colors and see if they add to zero? Or to loop over them one at a time and exit true
if we find a non-zero one?
I could surely create a bitmap and test whether I could correctly fetch bits out of it but come on, it’s get(x,y)
, how hard could it be?
Now GeePaw’s little videos (I’ve seen the first two) make me want to do a better job of splitting the view part of this game from the model part, the graphics from the logic. And I’ll try to focus more on that.
But so help me, I don’t see much separation to be had here: I don’t see a wide enough chunk of logic to really test much.
We’ll see.
For now …
Let’s Reduce Technical Debt
Technical debt, strictly speaking, is the difference between the design we have, and the design we now understand that we’d prefer to have.
Technical debt is not crappy code that we wrote because we were in a hurry. It’s not bugs we left in the system either on purpose or because time ran out. Those are bad things, and we have some of those in this program as well.
But technical debt is about the fact that even in our very bestest code, there can come to be a difference between what we have, and what we want.
What we have here, the more I think about it, is a combination, and as we go forward, I’ll try to talk about each bit and the extent to which it’s just ratty code, and the extent to which my design understanding has grown beyond what I did in the past.
Enough babbling. Let’s get to it.
Our big pile of um stuff is Main tab. It looks like this:
-- Invaders
-- RJ 20200803
function setup()
runTests()
createShields()
createBombTypes()
TheArmy = Army()
setupGunner()
Missile = {v=0, p=vec2(0,0)}
invaderNumber = 1
end
function draw()
pushMatrix()
pushStyle()
rectMode(CORNER)
spriteMode(CORNER)
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)
drawGrid()
TheArmy:draw()
drawGunner()
drawMissile()
drawShields()
popStyle()
popMatrix()
TheArmy:update()
end
function drawGrid()
pushStyle()
noFill()
--line(112,0,112,256)
rect(0,0,224,256)
popStyle()
end
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)}
}
end
function createShields()
local img = readImage(asset.shield)
local posX = 34
local posY = 24
Shields = {}
for s = 1,4 do
local entry = {img=img:copy(), pos=vec2(posX,posY), count=0, alive=true}
table.insert(Shields,entry)
posX = posX + 22 + 23
end
end
function drawShields()
for i,shield in ipairs(Shields) do
sprite(shield.img, shield.pos.x, shield.pos.y)
end
end
function explode()
Gunner.alive = false
Gunner.count = 120
end
function setupGunner()
Gunner = {pos=vec2(112,10)-vec2(8,4),alive=true,count=0,explode=explode}
GunMove = vec2(0,0)
end
function drawGunner()
pushMatrix()
pushStyle()
if Gunner.alive then
tint(255)
else
tint(255,0,0)
Gunner.count = Gunner.count - 1
if Gunner.count <= 0 then Gunner.alive = true end
end
sprite(asset.play,Gunner.pos.x,Gunner.pos.y)
stroke(255,0,0)
fill(255,0,0)
popStyle()
popMatrix()
Gunner.pos = Gunner.pos + GunMove
end
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)
if Missile.pos.y > 256 or TheArmy:checkForKill(Missile) then
Missile.v = 0
end
end
function runTests()
local det = CodeaUnit.detailed
CodeaUnit.detailed = false
Console = _.execute()
CodeaUnit.detailed = det
end
function showTests()
pushMatrix()
pushStyle()
fontSize(50)
textAlign(CENTER)
text(Console, WIDTH/2, HEIGHT-200)
popStyle()
popMatrix()
end
function touched(touch)
local fireTouch = 1171
local moveLeft = 97
local moveRight = 195
local moveStep = 0.25
local x = touch.pos.x
if touch.state == ENDED then
GunMove = vec2(0,0)
if x > fireTouch then
fireMissile()
end
end
if touch.state == BEGAN or touch.state == CHANGED then
if x < moveLeft then
GunMove = vec2(-moveStep,0)
elseif x > moveLeft and x < moveRight then
GunMove = vec2(moveStep,0)
end
end
end
function fireMissile()
if Missile.v == 0 then
Missile.pos = Gunner.pos + vec2(7,5)
Missile.v = 1
end
end
What have we here? We have, at least:
- Running the tests
- Setting up Shields
- Setting up Bombs
- Setting up the Army
- Setting up the Gunner
- Setting up the Missile
- Drawing the background
- Drawing the test results
- Drawing the grid
- Drawing the Army
- Drawing the Gunner
- Drawing the Missile
- Drawing the Shields
- Updating the Army
- Using Touches to move the Gunner
- Using Touches to fire the Missile
I mean what the $%#! man, all this stuff jammed together, what the $%#!?
Technical debt, or bad code?
Does it matter? Perhaps not. On the technical debt side, it can make good sense to keep somewhat disparate ideas close together until their separate natures start to come clear. On the bad code side, one “thing” that does all those sub-things … it’s not so good even though all the sub-things are broken out into functions.
Enough on the debate, call it whatever you need to call it to keep you from waking up screaming. Let’s make some small moves toward better. The way we get good is that we move toward better. We don’t have to get good all at once.
Now, you know my methods, and you therefore know that I like classes and methods more than I like stand-alone functions. So it should come as no surprise to you that I think we’d be better off if the Shields were instance of a class, like Bombs are, and maybe the Gunner should be a class, and so on.
But I’m not going to go there yet. Instead, I’m going to try something I’ve not tried before. I’m going to move the code relating to identifiable things, like Gunner and Shield, into separate tabs with appropriate names, but I’m not going to change the code at all. Not yet.
So I’ll do Shields first, because they’re set up first in Main. I find these two functions:
function createShields()
local img = readImage(asset.shield)
local posX = 34
local posY = 24
Shields = {}
for s = 1,4 do
local entry = {img=img:copy(), pos=vec2(posX,posY), count=0, alive=true}
table.insert(Shields,entry)
posX = posX + 22 + 23
end
end
function drawShields()
for i,shield in ipairs(Shields) do
sprite(shield.img, shield.pos.x, shield.pos.y)
end
end
And I’ll cut them from Main and paste them into a new tab called Shields.
-- Shields
-- RJ 20200819
function createShields()
local img = readImage(asset.shield)
local posX = 34
local posY = 24
Shields = {}
for s = 1,4 do
local entry = {img=img:copy(), pos=vec2(posX,posY), count=0, alive=true}
table.insert(Shields,entry)
posX = posX + 22 + 23
end
end
function drawShields()
for i,shield in ipairs(Shields) do
sprite(shield.img, shield.pos.x, shield.pos.y)
end
end
I put the comment there at the top, because I try always to remember to do that.
The program will probably still run.
It does but some tests fail. I can tell that two of them were failing before:
9: over right edge -- Actual: true, Expected: false
9: over right edge -- Actual: 220.0, Expected: 224
I need to make a bell ring or something if tests fail, the vague display doesn’t catch my eye. That test just needs its values updated.
_:test("over right edge", function()
local army = Army()
local testInvader = army.invaders[1]
army.invaders = {testInvader}
testInvader.pos.x = 220
army:doUpdate()
_:expect(army.overTheEdge).is(false)
_:expect(testInvader.pos.x).is(222)
army:doUpdate()
_:expect(testInvader.pos.x).is(224)
_:expect(army.overTheEdge).is(true)
end)
This test tries to position the invader just shy of the edge and then step her over. Our new edge is 207, so I think this should run:
_:test("over right edge", function()
local army = Army()
local testInvader = army.invaders[1]
army.invaders = {testInvader}
testInvader.pos.x = 203
army:doUpdate()
_:expect(army.overTheEdge).is(false)
_:expect(testInvader.pos.x).is(205)
army:doUpdate()
_:expect(testInvader.pos.x).is(207)
_:expect(army.overTheEdge).is(true)
end)
And it does. The other failures are these:
10: bombs get deleted -- Bomb:5: attempt to index a nil value (global 'BombTypes')
11: Bomb hits gunner -- Bomb:5: attempt to index a nil value (global 'BombTypes')
These tests are failing because our new code wants the global BombTypes to be in place, and it isn’t. We could call it in their init code, or we could put it in before
. Let’s do the latter.
_:before(function()
createBombTypes()
end)
The tests all pass and the game runs. Commit: create Shields tab, fix some tests.
Let’s see if there’s more shield stuff in Main. There seems not to be.
What about that createBombTypes
? It needs to be called from setup
or nearby but it should be in the Bomb tab. I’ll move it in:
-- Bomb
-- RJ 20200819
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)}
}
end
Bomb = class()
function Bomb:init(pos)
self.pos = pos
self.shapes = BombTypes[math.random(3)]
self.shape = 1
end
...
Everything still works, no surprise there. Commit: move bomb creation to Bomb tab.
We have three functions relating to the Gunner:
function explode()
Gunner.alive = false
Gunner.count = 120
end
function setupGunner()
Gunner = {pos=vec2(112,10)-vec2(8,4),alive=true,count=0,explode=explode}
GunMove = vec2(0,0)
end
function drawGunner()
pushMatrix()
pushStyle()
if Gunner.alive then
tint(255)
else
tint(255,0,0)
Gunner.count = Gunner.count - 1
if Gunner.count <= 0 then Gunner.alive = true end
end
sprite(asset.play,Gunner.pos.x,Gunner.pos.y)
stroke(255,0,0)
fill(255,0,0)
popStyle()
popMatrix()
Gunner.pos = Gunner.pos + GunMove
end
We’ll pop those into a new tab Gunner. Should still work … and it does. Tests OK too. Commit: Gunner code to Gunner tab.
In Main, we still have this, which relates only to the Gunner:
function touched(touch)
local fireTouch = 1171
local moveLeft = 97
local moveRight = 195
local moveStep = 0.25
local x = touch.pos.x
if touch.state == ENDED then
GunMove = vec2(0,0)
if x > fireTouch then
fireMissile()
end
end
if touch.state == BEGAN or touch.state == CHANGED then
if x < moveLeft then
GunMove = vec2(-moveStep,0)
elseif x > moveLeft and x < moveRight then
GunMove = vec2(moveStep,0)
end
end
end
I think we can move that wholesale into the Gunner tab. It’s a global function, so Codea should find it wherever it is. That works. Commit: Move touched to Gunner.
Main now has drawMissile
, and fireMissile
. fireMissile
is used only in touched
, so it should definitely move into Gunner, at least for now.
Everything still works.
I’m not certain what to do about drawMissile
. There should probably be a Missile tab and Missile object, but I’m not sure of it. So I’ll leave drawMissile
in Main, as an intentional eyesore, hoping that it’ll irritate me enough that I’ll fix it when I know how. (Hope is not a strategy, but it’s all I’ve got for you at the moment.)
Anyway commit: Moved fireMissile to Gunner.
This is all I see for this phase of improvement. We’ve broken out the Shields and Gunner into new tabs, making Main a lot nicer in the process. Maybe we’re done.
But hey! Look at the Shields tab:
-- Shields
-- RJ 20200819
function createShields()
local img = readImage(asset.shield)
local posX = 34
local posY = 24
Shields = {}
for s = 1,4 do
local entry = {img=img:copy(), pos=vec2(posX,posY), count=0, alive=true}
table.insert(Shields,entry)
posX = posX + 22 + 23
end
end
function drawShields()
for i,shield in ipairs(Shields) do
sprite(shield.img, shield.pos.x, shield.pos.y)
end
end
Where’s the code for colliding with the bombs? That’s in Bombs:
function Bomb:killedShield()
for i,shield in ipairs(Shields) do
local pos = shield.pos
local img = shield.img
local hit = self:damageShield(img,pos)
if hit then return true end
end
return false
end
function Bomb:damageShield(img, shieldPos)
local hit = rectanglesIntersectAt(self.pos,3,4, shieldPos,22,16)
if hit == nil then return false end
self:applyDamage(img, hit, shieldPos)
return true
end
function Bomb:applyDamage(img, hit, imagePos)
local relativePos = hit-imagePos
for x = -1,1 do
for y = -1,1 do
img:set(relativePos.x + x, relativePos.y + y, 255,0,0)
end
end
end
We have a bit of “feature envy” going on here. The Bomb is asking questions of the shield and even fiddling with its bitmap. That’s not good code by my standards, as we move away from the procedural approach to a more object-oriented approach.
I think shields are definitely asking to be made into first-class objects and their interaction with bombs wants to be sorted out.
Maybe we’ll do that next time. For now, let’s sum up.
Summing Up
We rather quickly fixed up our invaders going over the edge, and even adjusted their downward movement to be more menacing.
With time and energy to spare, we did a very simple division of the code in Main into two additional tabs, Shield and Gunner. We did nothing more than move related code into those tabs. There were no changes needed to make things work.
The work went very quickly. We did five commits in 15 minutes, and, before chai, three in less than 20 minutes. That’s eight commits, and each one made the product just a little bit better, either in external quality or internal quality.
As the guy who has to dig around here in the code, I’m sure that the tabs are going to help me even if we don’t go to classes and methods just yet, or ever. I’ll know where to look for things, and if I do find something in the wrong tab, I’ll have a place to put it.
Now I could have made those tabs sooner, and could have created a Shield object and a Gunner object right out of the box. But I wanted to get things going rapidly, and so I just made things out of squares, and drove them from Main, to get a sense of the game.
When I had an idea for a “thing”, like a missile, I tried to break out functions, at least, but not objects. I tried to name the functions to relate to the thing in question. (Sorry about “explode”.) This gave me a bit better ability to navigate than had everything just been done in line, and it made today’s work quite a bit easier than it would have otherwise been, because I could at least spot all the Shields stuff pretty readily.
For me, it’s no more trouble to name a function fireMissile
than to name it fire
, and it usually pays off. And if I had an object for the Missile, building Missile:fire
is easy too. So I try to break things out and give them meaningful names, because, for me, it usually takes no more time to do, it usually makes it easier to make the thing work, and it very often pays off in the future as the program gets more complex.
You may have similar habits, or you may not. What you have here, is an opportunity to think about your habits, and to adjust your behavior to emphasize the ones you like and get rid of the ones you don’t.
Now if I could just emphasize exercise and extinguish potato chips, I’d be even better off.
See you next time!