More with the saucer, that’s my plan.

I continue to be struck by my “feelings” about the stronger testing I’ve done in driving out the saucer code. This after two attempts that got reverted, one because it was really just a spike to get the sense of it, and one because I’d started building on a bad basis and it just took the wind out of my sails.

I never mind starting over when it’s just a few hours’ work. I’ve always learned things, some of them consciously and some not so much, and the next time through, things go better.

It makes me wonder what would happen if I did Space Invaders or Asteroids over from scratch. I think I did Spacewar at least twice, maybe three times.

With the saucer, I’ve got testing for more behavior than usual, including testing its starting location, ending location, move speed, and making sure it kills itself when it reaches the end of the path. I know from reading other people’s Codea code that I’m almost the only person who writes many tests at all, and surely the only one who tries to do TDD. With such a graphically-focused language, it’s easy to see when something looks right, and since so often all the positions and rotations and such are computed, it’s hard to test much of it.

For me, I get greater confidence in the code when I have tests, and it’s easier to change and improve it when the tests support me. Of course, it’s possible to have tests that are too tightly tied to the implementation, and then they get in the way of changes. There’s a balance to be found, and learning to be, well, learned.

Anyway, I’m noticing that the saucer tests help me feel good. So far. We’ll see what happens next.

And what should happen next?

One thing that happens now is that when the game starts, the saucer flies across from left to right and then stops. My tests tell me it’s not alive, but we aren’t checking that in draw. This will fix that:

function Saucer:draw()
    pushStyle()
    if self.alive then
        tint(255,0,0)
        sprite(asset.saucer, self.pos:unpack())
    end
    popStyle()
end

Now it flies across and disappears.

We were talking about testing. Would it even be possible to write a test that told us whether the saucer would stop drawing itself if it wasn’t alive? If possible, how hard would it be and how much would we hate it?

I can think of a couple of ways it could be done.

One would be to use Codea’s ability to draw into a bitmap. We could create a screen-sized bitmap, set up to draw into it, then call the draw function, then look at the bitmap to see if the saucer’s bits were there. Since it is generally drawn in a place where there’s nothing else, we could just check a few pixels looking for red. (The screen isn’t black: we’d have to deal with that.)

That seems kind of deep down in the system for a test, but the approach might be useful. For example, we could use it to test our code that detects actual collisions between the bits of sprites.

Another approach might go like this. Break out the draw function this way:

function Saucer:draw()
    pushStyle()
    if self.alive then
        self:actualDraw()
    end
    popStyle()
end

With the drawing code down in actualDraw. Then in the test, we could override our test saucer’s actualDraw with a proxy that would call back to the test if it was called. That would constitute an error.

This, too, seems like a long way to go when we can just look at the screen, but again it is a technique that could be useful.

I think what I’ve got going here is a better approach as long as it works. I’m being a bit more careful with what the object knows and how it operates, but not going nuts.

Anyway, what do we really need this thing to do?

We need to control when it flies. We need to condition its direction on the number of shots fired, which we do not have recorded at this time. We need to allow the player shots to kill it. We need to compute a variable score for killing it.

Let’s do the player shot count, just because it’s probably easy. We do need it, though it won’t be user-visible.

Where should we keep it? Probably the player should keep it because it does the firing:

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
        SoundPlayer:play("shoot")
    end
end

So let’s just init a shotsFired and tick it, plus provide an accessor. I’m going to make it a function because I’m trying that as a style to see if I prefer it. It could easily enough be accessed as a value of course.

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

function Player:shotsFired()
    return self.missileCount
end

Oh, hell, I should have written a test for that. I’ll do it now:

        _:test("Player counts shots fired", function()
            local Gunner = Player()
            _:expect(Gunner:shotsFired()).is(0)
            Gunner:fireMissile()
            _:expect(Gunner:shotsFired()).is(1)
        end)

That runs. But what if I had done this:

        _:test("Player counts shots fired", function()
            local Gunner = Player()
            _:expect(Gunner:shotsFired()).is(0)
            Gunner:fireMissile()
            _:expect(Gunner:shotsFired()).is(1)
            Gunner:fireMissile()
            _:expect(Gunner:shotsFired()).is(2)
        end)

Also a reasonable test. But it fails:

24: Player counts shots fired  -- Actual: 1, Expected: 2

Why? Because the missile is alive, so it cannot fire another. We could rip the Missile out of the player and kill it, or we could accept the original test as convincing us that we’ve done it right. I’m into not ripping things out today, so I’ll put the test back the way it was at first.

Tests are green, and we’re overdue for a commit: saucer disappears from screen and Player counts shots.

A Diversion

As if my life were not one of constant diversions anyway, I got some feedback from Dave1707 of the Codea forum. He was having some difficulty firing missiles. He said:

On my iPad Air, my screen height is 1112, so I can never be above 1171. I lowered the value so I could fire the missiles. Maybe instead of a hard coded value, it should be HEIGHT - some value so it works on different HEIGHT devices.

I’m not entirely sure about the height comment, because the game is intended to be played in landscape mode and the fire touch is off to the right, not high up. But be that as it may, we should condition the values to the screen size.

Upon trying to play in portrait mode, it’s certainly the case that the fire button is out of reach. I think we should condition it dynamically, because you can rotate the screen and switch back and forth between landscape and portrait. And I think the game isn’t centered in portrait mode either.

So a new story, something something adapt to screen height and width something.

First the touch code:

function Player:touched(touch)
    local fireTouch = 1171
    local moveLeft = 97
    local moveRight = 195
    local moveStep = 1.0
    local x = touch.pos.x
    if touch.state == ENDED then
        self.gunMove = vec2(0,0)
        if x > fireTouch then
            self:fireMissile()
        end
    end
    if touch.state == BEGAN or touch.state == CHANGED then
        if x < moveLeft then
            self.gunMove = vec2(-moveStep,0)
        elseif x > moveLeft and x < moveRight then
            self.gunMove = vec2(moveStep,0)
        end
    end
end

But let’s look at the general positioning as well:

function GameRunner:draw()
    pushMatrix()
    pushStyle()
    noSmooth()
    rectMode(CORNER)
    spriteMode(CORNER)
    stroke(255)
    fill(255)
    scale(4) -- makes the screen 1366/4 x 1024/4
    translate(WIDTH/8-112,0)
    TheArmy:draw()
    self.player:draw()
    drawShields()
    drawStatus()
    popStyle()
    popMatrix()
end

The translate should work, I think, to give us 224 size 4 pixels, centered. I’ll slip in a rectangle border for a while to check. Yes, we’re centered in both modes. I think this is telling me that I need only be concerned with the touch.

function Player:touched(touch)
    local fireTouch = WIDTH-195
    local moveLeft = 97
    ...

That seems to do the trick, I can play in portrait or landscape mode, Play equally poorly, I must add. I’m not sure how I’ll ever manage to shoot the saucer.

Let’s commit: adjust touch to screen WIDTH.

Moar Saucers

Right now, we just fly one saucer across right at the beginning of the game, and then nothing. Let’s see about flying more of them, but not too many.

There’s something in the original game about saucers only flying after the aliens drop down a bit, and watching gameplay, it looks like two drops. From another test, I think the bottom row of invaders starts at Y = 105. When they reverse direction, they drop by 8, so let’s say two reversals and saucers can fly. That will mean that invader 1’s Y position will be 105-16 or 89.

Where’s our saucer logic going to go? Basically it needs to go where we drop bombs:

function Army:possiblyDropBomb()
    if self:canDropBomb() then 
        self:dropRandomBomb()
    end
end

function Army:canDropBomb()
    return self.bombCycle > self.bombDropCycleLimit
end

function Army:dropRandomBomb()
    local bombs = {self.rollingBomb, self.plungerBomb, self.squiggleBomb}
    local bombType = math.random(3)
    self:dropBomb(bombs[bombType])
end

function Army:dropBomb(aBomb)
    if aBomb.alive then return end
    local col = aBomb:nextColumn(self)
    local y = self:lowestInvaderY(col)
    if y ~= nil then
        aBomb.pos = vec2(self:bombX(col), y - 16)
        self.bombCycle = 0
        aBomb.alive = true
    end
end

I think we’ll just do this, by intention:

function Army:dropRandomBomb()
    if not self:runSaucer() then
        local bombs = {self.rollingBomb, self.plungerBomb, self.squiggleBomb}
        local bombType = math.random(3)
        self:dropBomb(bombs[bombType])
    end
end

And make runSaucer return true/false:

function Army:runSaucer()
    if self.saucer.alive then return true end
    if self:yPosition() > 89 then return false end
    if math.random(1,20) < 20 then return false end
    self.saucer:go()
    return true
end

We don’t have Saucer:go yet. It just runs when created. We’ll need to change that, and that may break our tests. I should probably test-drive this bit but I’m tired.

We have this:

function Saucer:init(shotsFired)
    self.startPos = vec2(  8,185)
    self.stopPos  = vec2(208,185)
    self.step     = vec2(  2,  0)
    if shotsFired%2==1 then
        self.step = -self.step
        self.startPos,self.stopPos = self.stopPos,self.startPos
    end
    self.alive = true
    self.pos = self.startPos
end

I’ll extract that into go and leave the init alone. We’ll need either to provide shotsFired or fetch it. Best to provide it:

function Saucer:init()
end

function Saucer:go(shotsFired)
    self.startPos = vec2(  8,185)
    self.stopPos  = vec2(208,185)
    self.step     = vec2(  2,  0)
    if shotsFired%2==1 then
        self.step = -self.step
        self.startPos,self.stopPos = self.stopPos,self.startPos
    end
    self.alive = true
    self.pos = self.startPos
end

And back to the go call:

function Army:runSaucer()
    if self.saucer.alive then return true end
    if self:yPosition() > 89 then return false end
    if math.random(1,20) < 20 then return false end
    self.saucer:go(self.player:shotsFired())
    return true
end

I expect this to work but it’ll take a long time to turn up. Anyway we’ll need to fix the tests which will fail bigtime.

They do, but also this:

Saucer:31: attempt to perform arithmetic on a nil value (field 'pos')
stack traceback:
	Saucer:31: in method 'update'
	Army:144: in method 'update'
	GameRunner:44: in method 'update60ths'
	GameRunner:32: in method 'update'
	Main:31: in function 'draw'
function Saucer:update()
    local newPos = self.pos + self.step
    if newPos.x <= self.stopPos.x then
        self.pos = newPos
    else
        self.alive = false
    end
end

We’d best condition this on being alive, and set not alive on creation:

function Saucer:update()
    if not self.alive then return end
    local newPos = self.pos + self.step
    if newPos.x <= self.stopPos.x then
        self.pos = newPos
    else
        self.alive = false
    end
end

function Saucer:init()
    self.alive = false
end

And again we run the tests.

Here’s something amusing:

14 Passed, 0 Ignored, 10 Failed

But the display is green, not red. Why? I check for “0 Failed” to show green. I’ll have to think about that.

I want to make these run easily. First let’s make sure that where we create the real saucer, we don’t pass in the shotsFired, then make the saucer automatically “go” when it is given shotsFired at create time:

function Saucer:init(test)
    if test == nil then
        self.alive = false
    else
        self:go(test)
    end
end

I think this should fix all the tests. And it does. Now I can wait and see if saucers fly, I guess. I almost wish I had written a test for this but I’d still have to watch on screen. Maybe I should have set the probability higher.

OK, well. I’ve set the probability pretty low and I see the saucer traveling left when I’ve fired an even number, but not traveling right when I’ve fired an odd number. Let’s check our code a bit.

function Saucer:go(shotsFired)
    self.startPos = vec2(  8,185)
    self.stopPos  = vec2(208,185)
    self.step     = vec2(  2,  0)
    if shotsFired%2==1 then
        self.step = -self.step
        self.startPos,self.stopPos = self.stopPos,self.startPos
    end
    self.alive = true
    self.pos = self.startPos
end
function Saucer:update()
    if not self.alive then return end
    local newPos = self.pos + self.step
    if newPos.x <= self.stopPos.x then
        self.pos = newPos
    else
        self.alive = false
    end
end

That’s flat wrong. It only works going right. My vaunted tests aren’t solid enough. For now, I’m going to fix it and not the tests, as I’m low on time and cycles.

I think this will do it. Either way I’m due for a break:

function Saucer:update()
    if not self.alive then return end
    local newPos = self.pos + self.step
    if self.startPos.x < self.stopPos.x and newPos.x <= self.stopPos.x then
        self.pos = newPos
    elseif self.startPos.x > self.stopPos.x and newPos.x >= self.stopPos.x then
        self.pos = newPos
    else
        self.alive = false
    end
end

Good. Now it goes both ways. That’ll do for now. Commit: saucer flies both ways randomly after invaders two rows down.

I need a break and some foodz. Back soon …

Not So Soon

It’s well after lunch. I took a break, read a bit, then did just some quick study of the invaders code. I still don’t get it. Yet.

I got a couple of messages back and forth with Dave, and I understand better what needs to be done on smaller screens. Basically, we are fortunate that there is just one spot to change:

function GameRunner:draw()
    pushMatrix()
    pushStyle()
    noSmooth()
    rectMode(CORNER)
    spriteMode(CORNER)
    stroke(255)
    fill(255)
    scale(4) -- makes the screen 1366/4 x 1024/4
    translate(WIDTH/8-112,0)
    TheArmy:draw()
    self.player:draw()
    drawShields()
    drawStatus()
    popStyle()
    popMatrix()
end

That scale sets me to a scale that fits on my screen, which is 1366x1024. We need to be sensitive to the true WIDTH and HEIGHT, and scale to the smaller of the scales. I’ve tried some small random values and things look OK. So I think we can go with this:

function GameRunner:draw()
    pushMatrix()
    pushStyle()
    noSmooth()
    rectMode(CORNER)
    spriteMode(CORNER)
    stroke(255)
    fill(255)
    local vs = HEIGHT/256
    local hs = WIDTH/224
    scale(math.min(vs,hs))
    translate(WIDTH/8-112,0)
    TheArmy:draw()
    self.player:draw()
    drawShields()
    drawStatus()
    popStyle()
    popMatrix()
end

This looks good, so let’s inline the calculations:

function GameRunner:draw()
    pushMatrix()
    pushStyle()
    noSmooth()
    rectMode(CORNER)
    spriteMode(CORNER)
    stroke(255)
    fill(255)
    scale(math.min(HEIGHT/256, WIDTH/224))
    translate(WIDTH/8-112,0)
    TheArmy:draw()
    self.player:draw()
    drawShields()
    drawStatus()
    popStyle()
    popMatrix()
end

So if I’m right, which assumes facts not in evidence, the game should now scale correctly on any Codea-running device, even an iPhone. I should try that.

I think we’ll have an issue with the touch areas though,

They look like this:

function Player:touched(touch)
    local fireTouch = WIDTH-195
    local moveLeft = 97
    local moveRight = 195
    local moveStep = 1.0
    local x = touch.pos.x
    if touch.state == ENDED then
        self.gunMove = vec2(0,0)
        if x > fireTouch then
            self:fireMissile()
        end
    end
    if touch.state == BEGAN or touch.state == CHANGED then
        if x < moveLeft then
            self.gunMove = vec2(-moveStep,0)
        elseif x > moveLeft and x < moveRight then
            self.gunMove = vec2(moveStep,0)
        end
    end
end

For now, I think I’ll leave that alone. Probably need to rethink controls for small screens anyway. Maybe actual buttons along the bottom or something.

I’ll commit this: scale to screen size. And we should …

Sum Up

Today was a bit eclectic, with some improvement to the saucer, showing it deciding which way to go and going rather too often just now. It still needs more capability:

  • Correct timing
  • Locking out vis-a-vis rolling missile
  • Can be shot down
  • Scores correctly

But we have a bit more to show. In addition, we should be close to correct scaling, at least for the game playground. I think we should be using the maximum available space, based on actual width and height.

More to do there, probably, after I try to get this to run on my phone. And controls need work.

My tests for the saucer let me down a bit, not detecting the mistake in checking for past range. I’ll try to remember to beef them up a bit. It was just good fortune that I noticed the saucer was only going one way.

All in all, a decent day. A bit disorderly, but no periods of dismay or alarm, so it’s all good.

Invaders.zip