Improving zombie behavior. Sound adjustments. Maybe more. Maybe less.

Dave1707 from the Codea forum tried yesterday’s version–I’m glad someone is–and reported that the sound during attract mode was irritating, especially since the saucers basically fly all the time. So I plan a quick change for that. He also noticed that the zombie player can run off the screen, which I observed yesterday but didn’t take the time to fix. We’ll fix that as well.

We may also have to try to fix the 6 key on my Mac’s Magic Keyboard, or borrow my wife’s. A new one should arrive tomorrow. Keyboard, not wife.

Let’s see about the sound. There is a volume parameter in the call to sound, so let’s add a volume member and use it. Sound presently looks like this:

function Sound:init(noisy)
    self.noisy = true
    self:defineSounds()
    self:defineOminousTones()
end

function Sound:beSilent()
    self.noisy = false
end

function Sound:beNoisy()
    self.noisy = true
end

The noisy flag is used by the tests to keep them quiet. We can use volume, which ranges from zero to one, for that purpose, so let’s do this:

function Sound:init(noisy)
    self.volume = 1
    self:defineSounds()
    self:defineOminousTones()
end

function Sound:setVolume(vol)
    self.volume = vol
end

function Sound:beSilent()
    self:setVolume(0)
end

function Sound:beNoisy()
    self:setVolume(1)
end

Now let’s use the volume:

function Sound:play(aSoundName)
    self:playRaw(self.sounds[aSoundName])
end

function Sound:playRaw(aSound)
    sound(aSound, self.volume)
end

I expect everything to be the same at this point, and it is. Now let’s see if we can find a place to set the volume lower during zombie mode. Here’s one possibility:

function Player:spawn(zombie, pos)
    self.zombie = zombie or false
    self.zombieCount = 0
    self.zombieMotion = 0
    self.alive = not self.zombie
    self.drawingStrategy = self.drawPlayer
    self:setPos(pos)
end

We could check the zombie flag here and adjust the sound. But that seems kind of deep in the system to be making that global a decision. Let’s see how we call this method.

function GameRunner:gameOver()
    self:spawn(true)
end

This looks like a good place to turn it off. How does the first player start up?

The player sends requestSpawn to the GameRunner, which is:

function GameRunner:requestSpawn()
    self.lm:next()(self)
end

Life manager is initialized with these operations:

    self.lm = LifeManager(numberOfLives or 3, self.spawn, self.gameOver)

So we’ll call ‘spawn’ when the game is on, and otherwise gameOver. Unfortunately, right now gameOver calls spawn also, so we don’t quite have a place to put the sound call. I think we’ll let it reside in the Player for now, but I don’t feel perfect about it.

function Player:spawn(zombie, pos)
    self.zombie = zombie or false
    if self.zombie then
        Runner:soundPlayer():setVolume(0.05)
    else
        Runner:soundPlayer():setVolume(1.0)
    end
    self.zombieCount = 0
    self.zombieMotion = 0
    self.alive = not self.zombie
    self.drawingStrategy = self.drawPlayer
    self:setPos(pos)
end

It turns out that anything bigger than 0.05 is loud enough to be irritating in attract mode.

There is one other issue, which is that until the first player spawns in attract mode, the sound is set at 1. For now, I’ll let that ride. I’m semi-planning, which is slightly better than just hoping, that we’ll move knowledge of attract mode a bit higher up and we can deal with the sound then.

We’ll commit: sound at 0.05 during attract mode.

I’m getting a bit of sound during the tests. Let’s see what they do.

        _:before(function()
            Runner = GameRunner(3)
            Runner:soundPlayer():beSilent()
        end)

        _:after(function()
            Runner:soundPlayer():beNoisy()
            Runner = nil
            Player:clearInstance()
        end)

That looks good to me. Does someone else create a GameRunner in here? There’s one created on the fly but it’s not likely to make any noise:

        _:test("Screen scaling", function()
            local g = GameRunner()
            local sc,tr
            sc,tr = g:scaleAndTranslationValues(1366,1024)
            _:expect(sc).is(4)
            _:expect(tr).is((1366-4*224)/(2*4))
            sc,tr = g:scaleAndTranslationValues(834,1112)
            _:expect(sc).is(3.72, 0.01)
            _:expect(tr).is(0)
        end)

When we were using the noisy flag, there was no sound from the tests. This is a bit mysterious. I’m going to ignore it for now and move on. This feels a bit bad but my energy to chase this has dissipated. Sometimes I need a pair to pick up the slack.

Keep Zombies Penned Up

The zombie players can run off screen. I looked at this last night and the reason is pretty clear:

function Player:manageMotion()
    if self.alive then
        self:manageNormalMotion()
    elseif self.zombie then
        self:manageZombieMotion()
    end
end

function Player:manageNormalMotion()
    self.pos = self.pos + self.gunMove + vec2(self:effectOfGravity(),0)
    self.pos.x = math.max(math.min(self.pos.x,208),0)
end

function Player:manageZombieMotion()
    if math.random(10) > 5 then self:fireMissile() end
    if self.zombieCount > 0 then
        self.zombieCount = self.zombieCount - 1
        self.pos.x = self.pos.x + self.zombieMotion
    else
        self.zombieCount = math.random(10,20)
        self.zombieMotion = math.random(-1,1)
    end
end

The normal motion clamps the player position between 0 and 208. The zombie one does not. The quick fix without duplication is this:

function Player:manageMotion()
    if self.alive then
        self:manageNormalMotion()
    elseif self.zombie then
        self:manageZombieMotion()
    end
    self.pos.x = math.max(math.min(self.pos.x,208),0)
end

function Player:manageNormalMotion()
    self.pos = self.pos + self.gunMove + vec2(self:effectOfGravity(),0)
end

Now it clamps for both. Testing … and that works. Commit: clamp zombies onto screen.

I noticed something else last night: bombs don’t kill zombies. I think attract mode would be more interesting if the zombie player would get destroyed and respawn.

There must be a check for bombs killing player.

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

Sure enough, there it is. We’ll do this:

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

There’s a bit of feature envy here, with all these references to the player. We should be asking it questions, not inspecting its flags. Let’s first see that this works, however.

The zombie player does explode now, but firing keeps happening while it’s off screen.

function Player:fireMissile()
    if (self.alive or self.zombie) and self.missile.v == 0 then
        self:unconditionallyFireMissile()
    end
end

We really want to know if it’s exploding or off screen. There’s the somewhat mysterious count variable, that is non-zero when we’re exploding or off screen. We can check it here. I’m inclined to check it in addition to the other flags.

This is, of course, a sign that I’m not sure what the interactions are between alive, zombie, and count. In turn, that’s a sign that the code is not clear in this area. We’ll want to deal with that, but not today.

So …

function Player:fireMissile()
    if (self.count == 0 and (self.alive or self.zombie)) and self.missile.v == 0 then
        self:unconditionallyFireMissile()
    end
end

This does the right thing in the game, but it breaks a test:

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

I reckon that count isn’t zero here. But we can call the unconditional version just as well:

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

Tests are green. Commit: Zombie player explodes when hit.

Let’s sum up.

Summary

These changes went fairly smoothly, though not perfectly. There were little nitty-gritty details that had to be dealt with, changes in a few more places than seem right, and a few surprises.

This is very typical of code that has been around a while. We even ran into a place where I wasn’t really sure of just the right change, so I made a rather large one, the checking of count in determining whether we could fire a missile.

That is a sure sign of code that isn’t communicating well, and a nearly sure sign of code that isn’t organized as well as it might.

We recognized that yesterday, when we observed that the zombie flag and the alive flag have three meaningful states and one state that has no meaning: alive and zombie. The system is in attract mode when there are no lives left and the Player requests a respawn. The respawn happens … with a zombie. That happens very low down in the code and isn’t reflected higher up.

function GameRunner:gameOver()
    self:spawn(true)
end

function GameRunner:spawn(zombie)
    Player:instance():spawn(zombie)
    self:setWeaponsDelay()
end

Probably that parameter should be named isZombie, don’t you think? Let’s do that.

function GameRunner:gameOver()
    self:spawn(true)
end

function GameRunner:spawn(isZombie)
    Player:instance():spawn(isZombie)
    self:setWeaponsDelay()
end

Commit: rename isZombie parameter.

That doesn’t correct the whole program, however. We do, however, have a clear indication in GameRunner that the game is over, so there is a good prospect for cleaning this up a bit. We will leave that for another time and a clear head.

Feelings

I should mention feelings here. Not the soft gentle feeling one gets when the cat is particularly loving (or cold, you never really know with cats), but the tense feeling that one gets when the code isn’t quite right. It’s as if you’re carrying one more plate than you’re quite able to handle, or when it looks like that one screw is going to cross-thread, or when suddenly the road is incredibly slick and someone is stopped in front of you. (If you feel that badly when coding, consider reverting.)

The point is, when the code’s not right, you can feel it in your body. Maybe your neck gets tight or your stomach feels off. Maybe when the loving cat comes up you shout at it.

This tension is a sign that the code isn’t carrying the load that it should, so that your mind has to deal with more than it is comfortable dealing with. Yes, I know that the macho brogrammer can remember 16 million things without fumbling. I also know that the macho brogrammer makes more mistakes per unit time when the number of things gets above 7. Seven, not seven million.

I try to pay attention to tension and other feelings as I work. They are important clues, suggesting extra care, slowing down, more tests, even reverting and starting over.

We’re not in revert territory, but we do need some cleanup here on Aisle 5. Coming soon. See you then!

Invaders.zip