I think we need to work on treasures today.

Before we do, however, I want to note an idea from Bruce Onder:

@RonJeffries I think it would be great to flesh out combat and its support systems.

Damage system Healing potions Shield (absorbs 1 point of damage per attack) Sword (adds to P’s attack power) Experience Leveling Up

Great series so far!

He went on to suggest some more detailed notions, which are worth considering. But certainly we need to have valuable things for our player princess to find. I had in mind some +n weapons, probably swords because of our limited graphics, and some kind of shielding thing. Amulets might be good.

Anyway, we need to do something about keys being needed for chests, and about getting the contents.

The TileArbiter manages things like the player trying to step onto a tile containing a chest. The relevant control lines now are these:

    t[Chest][Player] = {moveTo=TileArbiter.refuseMove, action=Player.startActionWithChest}
    t[Key][Player] = {moveTo=TileArbiter.acceptMove, action=Player.startActionWithKey}

function Player:startActionWithKey(aKey)
    self.keys = self.keys + 1
    sound(asset.downloaded.A_Hero_s_Quest.Defensive_Cast_1)
    aKey:take()
end

function Player:startActionWithChest(aChest)
    aChest:open()
end

function Chest:open()
    self.pic = self.openPic
end

I guess that a chest needs to contain a treasure, and that the treasure should display when the chest is open. If we add the treasure to the tile, it will display on top of the chest. If it’s an active object, as is the chest, it can be taken, as are keys.

In fact, we could create the treasure randomly when we open the chest, no need to actually lug it around. Or we could just have a simple member variable in the chest that tells it what to create.

Let’s try adding a Health treasure to the cell when a chest is opened. We’ll ignore the key issue for now, though it should be trivial.

I’m inclined to make the treasures each their own class. They’ll be small, and there will be duplication, but otherwise there’d likely be lots of conditionals if we create a single treasure class.

Health = class()

function Health:init(aTile,runner)
    self.tile = aTile
    self.runner = runner
end

function Health:draw()
    pushStyle()
    spriteMode(CENTER)
    local g = self.tile:graphicCenter()
    sprite(asset.builtin.Planet_Cute.Heart,g.x,g.y, 50,50)
    popStyle()
end

Now on Chest:open:

function Chest:open()
    self.pic = self.openPic
    self.tile:addContents(Health(self.tile, self.runner))
end

heart

That looks nearly good. We should boost it upward a few pixels, perhaps. Ten up:

function Health:draw()
    pushStyle()
    spriteMode(CENTER)
    local g = self.tile:graphicCenter()
    sprite(asset.builtin.Planet_Cute.Heart,g.x,g.y+10, 50,50)
    popStyle()
end

The original heart size is 101x171. To keep it proportional, a better size would be 35 by 60.

function Health:draw()
    pushStyle()
    spriteMode(CENTER)
    local g = self.tile:graphicCenter()
    sprite(asset.builtin.Planet_Cute.Heart,g.x,g.y+10, 35,60)
    popStyle()
end

That looks good:

heart good

Now let’s give it to the player when they next step on the tile, and clear the tile while we’re at it.

    t[Heart] = {}
    t[Heart][Player] = {moveTo=TileArbiter.refuseMove, action=Player.startActionWithHealth}

And …

function Player:startActionWithHealth(aHealth)
    self.healthPoints = self.healthPoints + aHealth:points()
end

And …

function Health:init(aTile,runner, points)
    self.tile = aTile
    self.runner = runner
    self.points = points or 1
end

function Health:points()
    return self.points
end

This is all pretty hard wired but it should do the job. Of course, we have no display of player health as yet. Let’s do a quick and very dirty:

function Player:draw(tiny)
    local dx = -2
    local dy = -3
    pushMatrix()
    pushStyle()
    spriteMode(CORNER)
    local center = self:graphicCorner()
    if not self.alive then tint(0)    end
    if tiny then
        tint(255,0,0)
        sx,sy = 180,272
    else
        sx,sy = 80,136
    end
    sprite(asset.builtin.Planet_Cute.Character_Princess_Girl,center.x+dx,center.y+dy, sx,sy)
    text(self.healthPoints, center.x+dx, center.y+dy+ 100)
    popStyle()
    popMatrix()
end

This displays a small number near the princess’s head. Let’s make it larger:

    fontSize(fontSize()*2)
    text(self.healthPoints, center.x+dx, center.y+dy+ 100)

OK, yes, well, I meant to say Health, not Heart:

    t[Health] = {}
    t[Health][Player] = {moveTo=TileArbiter.refuseMove, action=Player.startActionWithHealth}

And I need this:

function Health:healthPoints()
    return self.points
end

I’m rushing and it’s causing me to make silly mistakes. I could do some tests here but it seems unnecessary.

I noticed that the player can move onto the chest tile. I thought that wasn’t supposed to happen. I don’t see why it might be allowed, maybe it was because of the crash.

I’ve gotta make a movie of this. In the movie below, every time the number goes up, I tried to move the player into the chest:

healthy

So that’s interesting. I suspect that two things are happening. One of them is that even though the chest is already open, trying to open it again will cause it to add another health to the contents:

function Chest:open()
    self.pic = self.openPic
    self.tile:addContents(Health(self.tile, self.runner))
end

This should be conditioned:

function Chest:open()
    if self:isOpen() then return end
    self.pic = self.openPic
    self.tile:addContents(Health(self.tile, self.runner))
end

But the other issue is that the tile is in the process of looping over its contents, and adding an item to it will cause that item to immediately be discovered and we’ll interact with it. We need to buffer the addContents somehow, I think.

OK, that was fiddly. I’ve changed this to remove the health item after use:

function Health:giveHealthPoints()
    self.tile:removeContents(self)
    return self.points
end

And modified addContents and draw in Tile:

function Tile:addContents(anEntity)
    self.newlyAdded[anEntity] = anEntity
end

function Tile:draw(tiny)
    pushMatrix()
    pushStyle()
    spriteMode(CORNER)
    tint(self:getTint(tiny))
    local sprites = self:getSprites(self:pos())
    local center = self:graphicCorner()
    for i,sp in ipairs(sprites) do
        sprite(sp,center.x,center.y,self.runner.tileSize)
    end
    for k,c in pairs(self.newlyAdded) do
        self.contents[k] = c
    end
    self.newlyAdded = {}
    for k,c in pairs(self.contents) do
        c:draw()
    end
    popStyle()
    popMatrix()
end

And now I expect it to work:

healthgood

That looks right. Commit: chests contain Health+1. Temporary health display over player.

Let’s Reflect

That was a bit fast and loose. I didn’t make any “serious” mistakes, but I made a lot of silly careless kinds of mistakes. I was sort of in a mode of just banging in what was needed, and I even tweaked a couple of things that really didn’t need it.

TDDing the feature would have slowed me down, and that would have been good in this case, except that this feature really doesn’t lend itself very well at all to TDD, as it has no real logic to speak of.

The problem with adding contents on the fly has bitten me in the past. Formally the rule is that you can assign a new value to an existing key, or remove it, but addition of keys is not OK. It seems that the new key may or may not appear during the current loop. So, we buffer additions. That seems to work. However this is now messy again:

function Tile:draw(tiny)
    pushMatrix()
    pushStyle()
    spriteMode(CORNER)
    tint(self:getTint(tiny))
    local sprites = self:getSprites(self:pos())
    local center = self:graphicCorner()
    for i,sp in ipairs(sprites) do
        sprite(sp,center.x,center.y,self.runner.tileSize)
    end
    for k,c in pairs(self.newlyAdded) do
        self.contents[k] = c
    end
    self.newlyAdded = {}
    for k,c in pairs(self.contents) do
        c:draw()
    end
    popStyle()
    popMatrix()
end

Let’s refactor a bit:

function Tile:draw(tiny)
    pushMatrix()
    pushStyle()
    spriteMode(CORNER)
    self:updateContents()
    tint(self:getTint(tiny))
    self:drawSprites()
    self:drawContents()
    popStyle()
    popMatrix()
end

function Tile:drawContents()
    for k,c in pairs(self.contents) do
        c:draw()
    end
end

function Tile:drawSprites()
    local sprites = self:getSprites(self:pos())
    local center = self:graphicCorner()
    for i,sp in ipairs(sprites) do
        sprite(sp,center.x,center.y,self.runner.tileSize)
    end
end

function Tile:updateContents()
    for k,c in pairs(self.newlyAdded) do
        self.contents[k] = c
    end
    self.newlyAdded = {}
end

Much nicer, by my standards.

In other news, I have failing tests. Since they don’t display in those big letters on the screen now, I tend to miss that they didn’t run. It may be necessary to improve the tool. Anyway let’s see what’s borked.

19: TileArbiter: player can step on key and receives it  -- Actual: table: 0x2940d4b40, Expected: table: 0x2940d4bc0

I rather fear that some of these may have been failing for a while. If so, an argument could be made that I’m a bad person.

Anyway this test is:

        _:test("TileArbiter: player can step on key and receives it", function()
            local runner = GameRunner()
            local room = Room(1,1,20,20, runner)
            local pt = Tile:room(11,10,runner)
            local player = Player(pt,runner)
            runner.player = player
            local kt = Tile:room(10,10, runner)
            local key = Key(kt,runner)
            _:expect(player.keys).is(0)
            _:expect(kt.contents).has(key)
            local arb = TileArbiter(key,player)
            local moveTo = arb:moveTo()
            _:expect(moveTo).is(kt)
            _:expect(player.keys).is(1)
            _:expect(kt.contents).hasnt(key)
        end)

There are three OKs after the fail, and one before, so it appears that the tile contents does not include the key. I’d better put some commentary in those expectations, to be sure. Oh. I know what it is, it’s the new add contents. Key does this:

function Key:init(tile, runner)
    self.tile = tile
    self.tile:addContents(self)
    self.runner = runner
end

And we don’t update contents until inside draw. So our tests aren’t seeing updates to contents.

One “easy” fix will be to change all the related tests to update. We just added a function to do it. I think a better way will be to set the original add contents back the way it was, and add a new function, addDeferredContents for the special case of a chest update.

function Tile:addContents(anEntity)
    self.contents[anEntity] = anEntity
end

function Tile:addDeferredContents(anEntity)
    self.newlyAdded[anEntity] = anEntity
end

function Chest:open()
    if self:isOpen() then return end
    self.pic = self.openPic
    self.tile:addDeferredContents(Health(self.tile, self.runner))
end

This seems to work just fine. Commit: added addDeferredContents for live additions to tiles.

In addition, all the tests are now running save only one:

13: monster can enter player tile  -- Actual: Tile[10][10]: room, Expected: Tile[11][10]: room

I think I changed that ages ago and never changed the test. Here is the TA entry:

    t[Player][Monster] = {moveTo=TileArbiter.refuseMoveIfResidentAlive, action=Monster.startActionWithPlayer}

So that test needs improvement:

        _:test("monster can enter player tile", function()
            local runner = GameRunner()
            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
            local chosenTile = monsterTile:validateMoveTo(monster,playerTile)
            _:expect(chosenTile).is(playerTile)
        end)

Let’s test both cases:

        _:test("monster can enter player tile only if player is dead", function()
            local chosenTile
            local runner = GameRunner()
            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(playerTile)
        end)

All tests run. Commit: modified test for current monster-player interaction.

I’m hungry. Let’s sum up and grab something to eat.

Summary

I was trying to go too fast, for no particular reason. I had done a few other things this morning, so maybe I wasn’t in as contemplative a mood as when I start first thing. It would have been wise to have noticed sooner and perhaps taken a break, or written a bit about the feeling. I think that working too fast is a bit like running downhill. You can get going too fast for your feet and start tumbling. The good news is that you rarely break a bone when coding too fast. The bad news is that you quite frequently break code.

Well, I do. I don’t know that you do, but I know how I’d bet.

Nonetheless, we have a decent but rudimentary implementation of chest contents. We’ll probably want to have GameRunner make the determination of what kinds of treasures should be provided, so that we can adjust the contents of levels according to some notion of difficulty. More healing hearts if there are lots of powerful monsters, and so on.

But that should be straightforward to do in the next phase.

Does it seem odd to you to do such a thin version of the treasures contained in chests? There’s no control over how many points the Health is worth, nor provision for anything other than Health. There’s hardly anything there.

To me, that’s the point. We have a big story in mind:

Chests can contain all kinds of treasures. Most of them will be good. Some may be bad. Treasures might include gold, health potions, gems of power, weapons, charming evening outfits, or pets. Provision should be made for artificial intelligence and teleportation.

To me, one of the most important tools in software development is to slice a big story down to a small essential first story that actually delivers visible progress toward the big story. Then we keep improving that capability as our product owners, managers, or whims may direct.

Thin is good. This is the way.

See you next time!


D2.zip