A time for healing. Gonna do it this time.

OK, the foundation is improved, let’s put in our feature about healing. The idea is that when the player’s health is below nominal, it slowly rises, and when it’s above, it slowly falls.

I’ve decided to use tween-delayfor this, so we’ll start with GameRunner, which already starts monster timers.

function GameRunner:setupMonsters(n)
    self.monsters = Monsters()
    self.monsters:createRandomMonsters(self, 6, self.dungeonLevel)
    self.monsters:startAllTimers()
end

I think I’d like to break out the timer starting and put it at the end of the createLevel:

function GameRunner:createLevel(count)
    self.dungeonLevel = self.dungeonLevel + 1
    if self.dungeonLevel > 4 then self.dungeonLevel = 4 end
    TileLock=false
    self:createTiles()
    self:clearLevel()
    self:createRandomRooms(count)
    self:connectRooms()
    self:convertEdgesToWalls()
    self:placePlayerInRoom1()
    self:placeWayDown()
    self:placeSpikes(5)
    self:setupMonsters(6)
    self.keys = self:createThings(Key,5)
    self:createThings(Chest,5)
    self:createLoots(10)
    self:createButtons()
    self.cofloater:runCrawl(self:initialCrawl(self.dungeonLevel))
    self.playerCanMove = true
    TileLock = true
end

That becomes …

...
    self:createLoots(10)
    self:createButtons()
    self.cofloater:runCrawl(self:initialCrawl(self.dungeonLevel))
    self:startTimers()
    self.playerCanMove = true
    TileLock = true
end

Then:

function GameRunner:startTimers()
    self.monsters:startAllTimers()
    self.player:startAllTimers()
end

And:

function Player:startAllTimers()
    self._characterSheet:startAllTimers()
end

And finally we’re at a point where we can do something:

function CharacterSheet:startAllTimers()
    self._delay = tween.delay(5, self.adjustValues, self)
end

function CharacterSheet:adjust()
    self._health:adjust()
    self:startAllTimers()
end

Well, almost at the point where we can do something. Now we can write a test:

        _:test("HealthAttribute adjust toward nominal", function()
            local ha <const> = HealthAttribute(10)
            ha:accumulateDamage(2)
            ha:applyAccumulatedDamage(2)
            _:expect(ha:value()).is(8)
            ha:adjust()
            _:expect(ha:value()).is(9)
        end)

This should fail lacking adjust.

6: HealthAttribute adjust toward nominal -- Health:73: attempt to call a nil value (method 'adjust')

Now:

function CharacterSheet:adjustValues()
    self._health:adjust()
    self:startAllTimers()
end

function HealthAttribute:init(value, nominal)
    self._value = value
    self._nominal = nominal or value
    self._accumulatedDamage = 0
    self._adjustment = 1
end

I apologize for trying to make both sides work all at once. I just went wild. Test runs. Test more:

        _:test("HealthAttribute adjust toward nominal", function()
            local ha <const> = HealthAttribute(10)
            ha:accumulateDamage(2)
            ha:applyAccumulatedDamage(2)
            _:expect(ha:value()).is(8)
            ha:adjust()
            _:expect(ha:value()).is(9)
            ha:addPoints(3)
            _:expect(ha:value()).is(12)
            ha:adjust()
            _:expect(ha:value()).is(11)
        end)

I expect this to run. And it does. Now I expect to see the princess’s health rise when she’s below nominal and fall when she’s above.

healing

I do see the healing. It’s tempting to put a message in the crawl whenever the value ticks, but I think that would get busy when all her parameters are moving up and down. The timing of 5 seconds feels long but that means it would only take her about a minute to heal from zero.

Which makes me wonder what will happen if she dies. Will she come back to life? That would be nice. Let’s commit: player health adjusts toward nominal at 5 second intervals.

The answer to the question is that, yes, she does come back to life. It’s almost immediate, I suppose with an expected delay of 2.5 seconds after death. That’s fine for now.

What shall we do now? Let’s convert the strength and speed to attributes. As a first step, we should rename HealthAttribute to CharacterAttribute.

I’m modifying the code as I go, doing things like this:

function Player:retainedAttributes()
    return { keys=0, _health = CharacterAttribute(12,12), _speed = CharacterAttribute(8,8), _strength = CharacterAttribute(10,10) }
end

This is a bit risky but I think it’ll sort out quickly. If not, I’ll revert and go more slowly.

function Entity:healthAttribute()
    return self._characterSheet._health
end

function Entity:speedAttribute()
    return self._characterSheet._speed
end

function Entity:strengthAttribute()
    return self._characterSheet._strength
end

I’ve even changed the setters for creating the CharacterSheet, so I’d better have a look at how those adjust.

function CharacterSheet:pointsTable(kind)
    local t = {Health="_health", Strength="_strength", Speed="_speed"}
    return t[kind]
end

function CharacterSheet:addPoints(kind,amount)
    local name = self:pointsTable(kind)
    self[name]:addPoints(amount)
end

I’m going to run this and see what explodes. This isn’t very scientific, I admit.

Some tests fail. That’s good news.

1: Create CharacterSheet  -- Actual: nil, Expected: 10
        _:test("Create CharacterSheet", function()
            local cs = CharacterSheet()
            _:expect(cs:keyCount()).is(0)
            _:expect(cs:strength()).is(10)
            _:expect(cs:speed()).is(8)
            _:expect(cs:health()).is(12)
        end)

Uncorrected methods:

function CharacterSheet:speed()
    return self._speed:value()
end

function CharacterSheet:strength()
    return self._strength:value()
end

Test again. That breaks this:

12: monster can't enter player tile even if player is dead -- CharacterSheet:115: attempt to index a nil value (field '_speed')

The other tests run so I’d best see what’s going on in the test.

        _:test("monster can't enter player tile even if player is dead", function()
            local chosenTile
            local runner = Runner
            local monsterTile = Tile:room(10,10,runner)
            local monster = Monster(monsterTile, runner)
            local playerTile = Tile:room(11,10,runner)
            local player = Player(playerTile,runner)
            runner.player = player -- needed because of monster decisions
            chosenTile = monsterTile:validateMoveTo(monster,playerTile)
            _:expect(chosenTile).is(monsterTile)
            player:die()
            chosenTile = monsterTile:validateMoveTo(monster,playerTile)
            _:expect(chosenTile).is(monsterTile)
        end)

Ah. Monsters didn’t get converted:

function Monster:initAttributes()
    local health = CharacterAttribute(self:roll(self.mtEntry.health))
    local strengthPoints = self:roll(self.mtEntry.strength)
    local speedPoints = self:roll(self.mtEntry.speed)
    local attrs = { _health=health, strengthPoints=strengthPoints, speedPoints=speedPoints }
    self._characterSheet = CharacterSheet(attrs)
end

That becomes:

function Monster:initAttributes()
    local health = CharacterAttribute(self:roll(self.mtEntry.health))
    local strength = CharacterAttribute(self:roll(self.mtEntry.strength))
    local speed = CharacterAttribute(self:roll(self.mtEntry.speed))
    local attrs = { _health=health, _strength=strength, _speedPoints=speed }
    self._characterSheet = CharacterSheet(attrs)
end

I’m a bit troubled by the fact that creators of CharacterSheets have to know the code words _health and such. Right now, we’re on a mission to make this work. Then we’ll see about making it more right.

12: monster can't enter player tile even if player is dead -- CharacterSheet:115: attempt to index a nil value (field '_speed')

Did I type … yes I did:

function Monster:initAttributes()
    local health = CharacterAttribute(self:roll(self.mtEntry.health))
    local strength = CharacterAttribute(self:roll(self.mtEntry.strength))
    local speed = CharacterAttribute(self:roll(self.mtEntry.speed))
    local attrs = { _health=health, _strength=strength, _speed=speed }
    self._characterSheet = CharacterSheet(attrs)
end

Now we should be green. And the game plays correctly. Again the tests drive out all the necessary changes. I’m feeling better about my tests than I did in the past.

Commit: speed and strength use CharacterAttribute (renamed from HealthAttribute).

I think that’ll do it for this morning. Let’s sum up.

Summary

The adjustment feature went in nicely. I implemented and verified speed and strength adjustments as well as health. They can only adjust downward, making power-ups only last for a little while. I propose to add them to inventory for use on demand, rather than having them immediately applied, but that’s for the future.

The timer setup is interesting, as it goes from GameRunner to Player to CharacterSheet, and then from there, timed adjustment calls are made to each of the relevant CharacterAttributes. That all worked as intended, pretty much out of the box.

Converting the name of HealthAttribute to CharacterAttribute went well also. I took a bit of a risk in doing it all in one bite, but the tests discovered all the mistakes, as they are supposed to do. Second day in a row where the tests have driven out a successful implementation of something, so at least some areas of the program have a pretty decent safety net of tests.

Historically with Codea, I’ve not done as much testing as I would advise, if someone asked for advice and if I gave advice, which I don’t. I don’t mind saying that I wish I had better tests. The reasons why include:

  • I am a human being. I mean well, but I am lazy, to paraphrase Alistair Cockburn1.
  • Much of the Codea work I’ve done has been graphical in character, and that’s hard to test with automated tests.
  • CodeaUnit makes it hard to find a small number of broken tests among the working ones, although setting the flags differently may help a bit with that.
  • I am a human being. I mean well, but I sometimes make poor decisions.

Nonetheless, I’ve been bearing down a bit on the testing, and while it is a bit tedious, it definitely pays off when I’m refactoring, and of course TDD invariably pays off on new implementations.

See you next time!


D2.zip


  1. “Pronounced Co-burn, in the Scottish fashion.” – Alistair Cockburn