< A couple of small changes to CodeaUnit and my CUBase template. Then let’s see what we can improve about Space Invaders.

Saturday: There’s a defect here. I’ll explain in the next article. Zip file removed.

I’ll spare you the details, but I made a few tiny improvements to my CUBase template for setting up projects with CodeaUnit, and one associated change to CodeaUnit itself:

  • CodeaUnit expect function now accepts an optional message;
  • runTests and showTests renamed and moved into the Test tab
  • Main tab simplified down to a minimum.

I’ll include a zipped CUBase and CodeaUnit just on the off chance that anyone out there is actually following along with actual code.

CUBase.zipCodeaUnit.zip

Just for fun, I’ll add a message to one of my long patches of expectations, since that’s why I added the feature, and make it fail:

        _:test("finding bottom-most invader", function()
            local army = Army()
            local y
            y = army:lowestInvaderY(1)
            _:expect(y).is(105)
            army:directKillInvader(1)
            _:expect(army:lowestInvaderY(1)).is(121)
            _:expect(army:lowestInvaderY(2), "should be 105").is(107)
            army:directKillInvader(1 + 11)
            _:expect(army:lowestInvaderY(1)).is(137)
            army:directKillInvader(1 + 2*11)
            _:expect(army:lowestInvaderY(1)).is(153)
            army:directKillInvader(1 + 3*11)
            _:expect(army:lowestInvaderY(1)).is(169)
            army:directKillInvader(1 + 4*11)
            _:expect(army:lowestInvaderY(1)).is(nil)
        end)

Sure enough, it fails:

fail message

You can just about see it in the fine print there on the left:

20: finding bottom-most invader should be 105 -- Actual: 105.0, Expected: 107

Nothing fancy, but enough to do the job.

OK …

What shall we do today?

I think that today we should browse the code and look for things that could be better. One thing in particular comes to mind: I’ve taken recently to creating methods where I would previously have accessed an attribute, even when the method really does return a constant.

There are some advantages to this approach. First, if we did it uniformly, then there’d only be one option for talking to any object, sending it a message, and we wouldn’t have to remember whether this one was one of the message ones or the attribute ones. They’d all be message ones.

Second, when we export an attribute, we’re stuck with it. We’re less stuck with a method. Yes, we need to retain the name and meaning, but we can implement that meaning in any fashion we choose. We gain some flexibility, and from a programming viewpoint, it’s free. We’re not coding in some great general thing. We just always call methods and they always do what we need, however it’s done.

There is one drawback: a method is a function call. It does the same fetch from the object’s table as an attribute access, but then it calls what comes back instead of returning it. So a method call is slower than an attribute access.

I choose to entirely ignore this concern. If the program ever needs to be faster, we probably won’t get that speed by switching to accessors, we’ll more likely get it by using a better algorithm, and the method approach actually makes changing the algorithm easier.

Plus, if you’re still not bought in, come back and tell me the speed of a Codea attribute access versus a function call. If, after seeing those two tiny numbers, you still care.

So we’ll be looking for attribute accesses.

Another issue is globals. Our tests have given us problems because there are a fair number of global objects around, and random people are accessing them, sometimes to our detriment. We’‘ll stay alert for them and see whether we can hide most of them. Quite likely the GameRunner will be a good place for some, and possibly Army will be another good hiding place. We’ll see.

Finally, we have mostly moved to objects over global tables with global functions. I believe the outstanding exception is Gunner. Having thought of it, I choose that as the first thing to deal with. Here’s Gunner tab:

-- Gunner
-- RJ 20200819

function 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
        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 explode()
    Gunner.alive = false
    Gunner.count = 240
    SoundPlayer:play("explosion")
end

function setupGunner()
    local missile = Missile()
    Gunner = {pos=vec2(104,32),alive=true,count=0,explode=explode,missile=missile}
    GunMove = vec2(0,0)
    GunnerEx1 = readImage(asset.playx1)
    GunnerEx2 = readImage(asset.playx2)
end

function drawGunner()
    pushMatrix()
    pushStyle()
    Gunner.missile:draw()
    tint(0,255,0)
    if Gunner.alive then
        sprite(asset.play,Gunner.pos.x,Gunner.pos.y)
    else
        if Gunner.count > 210 then
            local c = Gunner.count//8
            if c%2 == 0 then
                sprite(GunnerEx1, Gunner.pos.x, Gunner.pos.y)
            else
                sprite(GunnerEx2, Gunner.pos.x, Gunner.pos.y)
            end
        end
        Gunner.count = Gunner.count - 1
        if Gunner.count <= 0 then
            if Lives > 0 then
                Lives = Lives -1
                if Lives > 0 then
                    Gunner.alive = true
                end
            end
        end
    end
    popStyle()
    popMatrix()
end

function updateGunner()
    Gunner.missile:update()
    if Gunner.alive then
        Gunner.pos = Gunner.pos + GunMove + vec2(effectOfGravity(),0)
        Gunner.pos.x = math.max(math.min(Gunner.pos.x,208),0)
    end
end

function effectOfGravity()
    local effect = 0.1
    local gx = Gravity.x
    return gx > effect and 1 or gx < -effect and -1 or 0
end

function fireMissile()
    if not Gunner.alive then return end
    local missile = Gunner.missile
    if missile.v == 0 then
        missile.pos = Gunner.pos + vec2(7,5)
        missile.v = 1
        SoundPlayer:play("shoot")
    end
end

Let’s go ahead and convert this thing to an object. We’ll have to look for callers of each of those functions, but that aside, and converting Gunner to self a lot, this should be straightforward. (cf. “Hold my beer.”)

One issue is that presently we use Gunner as the global name of the instance. That tempts me to name the class Player, which is more the Space Invaders thing to do. Yeah, let’s do that, and we’ll of course rename the tab as well.

I naively convert this tab to an object:

-- Player
-- RJ 20200819
-- made Player class 20200911

Player = class()    

function Player:init(pos)
    self.pos = pos or vec2(104,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)
end

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

function Player:explode()
    self.alive = false
    self.count = 240
    SoundPlayer:play("explosion")
end


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

function Player:update()
    self.missile:update()
    if self.alive then
        self.pos = self.pos + self.gunMove + vec2(self:effectOfGravity(),0)
        self.pos.x = math.max(math.min(self.pos.x,208),0)
    end
end

function Player:effectOfGravity()
    local effect = 0.1
    local gx = Gravity.x
    return gx > effect and 1 or gx < -effect and -1 or 0
end

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

Now I’ll go find all the references to Gunner and do something about them.

In Main, I just create a player called Gunner. That seems likely to be useful:

function setup()
    Runner = GameRunner()
    runTests()
    SoundPlayer = Sound()
    createShields()
    createBombTypes()
    Gunner = Player() -- <---
    invaderNumber = 1
    Lives = 3
    Score = 0
    Line = image(208,1)
    for x = 1,208 do
        Line:set(x,1,255, 255, 255)
    end
end

I also need a touched function now, because I converted the Player one to a method.

function touched(touch)
    Gunner:touched(touch)
end

GameRunner draws the Gunner:

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()
    Gunner:draw() -- <---
    drawShields()
    drawStatus()
    popStyle()
    popMatrix()
end

But, oddly, the Army updates it:

function Army:update()
    Gunner: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)
end

The Army also references the Gunner’s position. For now we’ll allow it:

function Army:targetedColumn()
    local gunnerX = Gunner.pos.x -- <---
    local armyX = self:xPosition()
    local relativeX = gunnerX - armyX
    local result = 1 + relativeX//16
    return math.max(math.min(result,11),1)
end

Yes, I’d like that to be a method, as discussed above. But right now, we’re refactoring, not changing implementation, so that would be a diversion. We’re smart enough to pull it off, but also smart enough not to try.

Bombs can kill the gunner:

function Bomb:killsGunner()
    if not Gunner.alive then return false end
    local hit = rectanglesIntersectAt(self.pos,3,4, Gunner.pos,16,8)
    if hit == nil then return false end
    Gunner:explode()
    return true
end

Here again, we think this will work and we’re not going to quibble over the attribute access.

The rectangles stuff bugs me and I think the objects should be asked for their collision information and provide it. Again, that’s not for just now.

The tests have a number of references, and I think I’ll let them tell me what needs changing by running them. I think I’ve done all I can find by hand. Let’s see what happens.

Delightfully, the game runs. And only two tests fail:

9: Bomb hits gunner -- Tests:72: attempt to call a nil value (global 'setupGunner')
19: rolling shot finds correct column -- Tests:188: attempt to call a nil value (global 'setupGunner')

With some luck, we can replace those with

    Gunner = Player()

And back away slowly.

Yiss! All the tests pass and the game runs.

I think we’ll commit at this point: Gunner = Player() class instance.

Now how about getting rid of that global. It seems to me that the game runner should own the player, and pretty much everything else. We’ll try that and see what happens.

We remove the definition of the Gunner global from Main. In GameRunner:

function GameRunner:init()
    TheArmy = Army()
    self.player = Player()
    self:resetTimeToUpdate()
end

I see another global there to go after. One thing at a time, Ron, one thing at a time.

In draw:

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

We noticed that Gunner got updated in Army: let’s move that up into GameRunner:

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

Army has a number of references to the Gunner global that we need to replace somehow. How about if we pass the gunner to the army when we create him, since he needs it.

function Army:init(player)
    self.player = player
    local vader11 = readImage(asset.inv11)
    ...

function Army:targetedColumn()
    local gunnerX = self.player.pos.x
    local armyX = self:xPosition()
    local relativeX = gunnerX - armyX
    local result = 1 + relativeX//16
    return math.max(math.min(result,11),1)
end

The bombs know the gunner as well. They’re pretty far dow in the stack of objects, and they already have a lot of parameters with their shapes and explosions and such. For now, we’ll let them ask the GameRunner, which may wind up being nearly our only necessary global, unless we want to pass it around everywhere. (Which could be the right thing, even if I’m not going to do it.)

function Bomb:killsGunner()
    if not Runner.player.alive then return false end
    local hit = rectanglesIntersectAt(self.pos,3,4, Runner.player.pos,16,8)
    if hit == nil then return false end
    Runner.player:explode()
    return true
end

There’s more than a little feature envy there. We may revist that one day.

The tests both make a Gunner, but they do it as a global. That’s troubling and could make weird stuff happen now that we don’t really want a global of that name. We’ll try making it a local to the tests first.

A bunch of tests fail. We’ll chase those shortly. Also an error as the game runs:

Army:112: attempt to index a nil value (field 'player')
stack traceback:
	Army:112: in function <Army:111>
	(...tail calls...)
	Army:88: in method 'dropBomb'
	Army:59: in method 'dropRandomBomb'
	Army:48: in method 'possiblyDropBomb'
	Army:125: in method 'update'
	GameRunner:44: in method 'update60ths'
	GameRunner:32: in method 'update'
	Main:31: in function 'draw'

That looks like a problem in at least one test as well. The issue is that Army expects to be handed a Player and isn’t.

function GameRunner:init()
    self.player = Player()
    TheArmy = Army(self.player)
    self:resetTimeToUpdate()
end

This will be true for any other creators of Army instances, such as in the tests. Some of the tests won’t care, others will. I’ll let the tests tell me.

19: rolling shot finds correct column -- Army:112: attempt to index a nil value (field 'player')
        _:test("rolling shot finds correct column", function()
            local army = Army()
            local Gunner = Player()
            local bomb = army.rollingBomb
            ...

Needs to be:

        _:test("rolling shot finds correct column", function()
            local Gunner = Player()
            local army = Army(Gunner)
            local bomb = army.rollingBomb
            ...

9: Bomb hits gunner – Actual: false, Expected: true ~~~

This one may need more work.

        _:test("Bomb hits gunner", function()
            local Gunner = 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)

Well. Bomb:killsGunner needs a live player in Runner. So we’ll need to create at least a GameRunner here.

        _:test("Bomb hits gunner", function()
            Runner = GameRunner()
            local Gunner = Runner.player
            Gunner.pos=vec2(50,50)
            ...

I’m hopeful but not certain about that. We’ll see. Ha! Tests all pass. Commit: remove Gunner global, player in GameRunner.

We could probably save some setup in the tests by moving some things to before and after, but that’s for another day. Ideally, we’d use several different setups, but one might suffice. Be that as it may, we’re done for now, except for the summing up.

Summing Up

A fairly simple process of creating a class, using it directly “all over”, then centralizing its definition into a single class (GameRunner) has resulted in removal of a global variable and a bit of simplification in the code.

A good morning’s work.

The tests carried some weight today, and did it nicely. They did find some problems for us in the actual code, and the issues with changing them over to the new scheme were fairly minimal.

It’s worth pointing out that the issues with the tests revolved around two main things. First, we had a global and used it a lot in the tests. Second, We didn’t centralize the creation of that global within the tests. We could have had a better test design without much trouble and that would have assisted in the process by requiring fewer edits to get all the tests running again.

As for the first issue, the very existence of the globals, we should stay more alert to that. Globals mess up the code, making surprise connections among objects that will come back to bite us. This shows up often in the tests, which makes them harder to maintain, which induces us to write fewer tests and remove them when they irritate us.

However, it’s not at all clear at the beginning where something should go, and a global finesses that decision, deferring it into the future. But it also guarantees work in the future. We’re not deeply troubled by that: we are here to harvest the value of change, not to avoid it.

Except that by programming in certain ways, we may be able to keep our rapid progress, have even more flexibility for future change harvesting, and make the inevitable rework less arduous. How might we do that?

Well, what if instead of defining global variables, we defined global functions that returned the object that’s needed? Then we could change anything underneath that function. We could even create a whole different kind of object to handle things internally, and return an adapter to folks who called our “legacy global”.

It might be worth trying that for a while, to see if it’s helpful. My intuition says that it might be. That’s enough to get me to try it, because it’s clear that it can’t be much worse:

function Gunner()
    return TheGunner
end

That’s so close to having the global that it’s clearly no more costly to do, nor more costly to move around, and it gives us this kind of flexibility all over:

function Gunner()
    return Runner.player
end

Hm. And shouldn’t that be Runner:player() as well, getting rid of those accessors that I was complaining about?

Perhaps it should. But that’s for another day!

See you then!

* Saturday: Zip removed.*