While I wait to see whether Santa came, I want to enhance Monster movement a bit. This will require the TileArbiter table and code to change.

Monsters do not move onto a tile with another monster, because it looks weird when they do. However, after a monster has died and appears in that dark-looking pile, other monsters still will not enter that space. That means that the player can hide behind the dead monster and avoid attack. Let’s change things so that monsters can enter tiles containing dead monsters, but not live ones.

TileArbiter’s table looks like this:

function TileArbiter:createTable()
    if TA_table then return end
    local t = {}
    t[Chest] = {}
    t[Chest][Monster] = {moveTo=TileArbiter.refuseMove}
    t[Chest][Player] = {moveTo=TileArbiter.refuseMove, action=Player.startActionWithChest}
    t[Key] = {}
    t[Key][Monster] = {moveTo=TileArbiter.acceptMove}
    t[Key][Player] = {moveTo=TileArbiter.acceptMove, action=Player.startActionWithKey}
    t[Player] = {}
    t[Player][Monster] = {moveTo=TileArbiter.acceptMove, action=Monster.startActionWithPlayer}
    t[Monster]={}
    t[Monster][Monster] = {moveTo=TileArbiter.refuseMove, action=Monster.startActionWithMonster}
    t[Monster][Player] = {moveTo=TileArbiter.acceptMove, action=Player.startActionWithMonster}
    TA_table = t
end

The accept and refuse methods are:

function TileArbiter:acceptMove()
    return self.resident:getTile()
end

function TileArbiter:refuseMove()
    return self.mover:getTile()
end

We now need a conditional move. It could be named acceptIfResidentDead or refuseIfResidentAlive. It could also be named neverCapitalizeGiraffe, but that would make no sense. I think it makes marginally more sense to ask whether the resident is alive, so let’s do this:

    t[Monster][Monster] = {moveTo=TileArbiter.refuseMoveIfResidentAlive, action=Monster.startActionWithMonster}
 
function TileArbiter:refuseMoveIfResidentAlive()
    if self.resident:isAlive() then
        self:refuseMove()
    else
        self:acceptMove()
    end
end

function Monster:isAlive()
    return self.alive
end

I think this is enough to do the job. This will be hard to test in the game, so I guess I’ll write a test. Should have done it first, but no matter.

        _:test("TileArbiter: monster can step on dead monster", 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 m1t = Tile:room(9,10)
            local monsterRes = Monster(m1t, runner)
            local m2t = Tile:room(10,10)
            local monsterMov = Monster(m2t, runner)
            local arb = TileArbiter(monsterRes, monsterMov)
            local moveTo = arb:moveTo()
            _:expect(moveTo).is(m2t)
        end)

I really expected this to work, but I got this:

20: TileArbiter: monster can step on dead monster -- ...in pairs(tweens) do
    c = c + 1
  end
  return c
end

:559: attempt to index a nil value (local 'id')

It appears to be running the tweens, but I thought we had fixed it to that those didn’t run until the game was started, by calling this function:

function Monster:startAllTimers()
    self:setAnimationTimer()
    self:setMotionTimer()
end

That’s called only in createLevel, which is called only in the Main tab, after running the tests:

function setup()
    if CodeaUnit then 
        codeaTestsVisible(true)
        runCodeaUnitTests() 
    end
    showKeyboard()
    TileLock = false
    Runner = GameRunner()
    Runner:createLevel(12)
    --Runner:createTestRooms()
    DisplayToggle = true
    TileLock = true
    local seed = math.random(1234567)
    print("seed=",seed)
    math.randomseed(seed)
end

However, there is another tween access:

function Monster:rest()
    tween.stop(self.motionTimer)
    self:setMotionTimer(5.0)
end

This doesn’t check to see if there is a motion timer. That’s probably the bug. Let’s do this:

function Monster:rest()
    if self.motionTimer then
        tween.stop(self.motionTimer)
        self:setMotionTimer(5.0)
    end
end

Now if we don’t have one, we don’t try to stop it and we don’t start one. With luck, that’ll fix this problem, since we know that when a monster tries to enter upon another, one of them does get told to rest.

Ah, much better:

20: TileArbiter: monster can step on dead monster  -- Actual: nil, Expected: Tile[10][10]: room

I think I forgot to return the values. Sure enough. This should fix us up:

function TileArbiter:refuseMoveIfResidentAlive()
    if self.resident:isAlive() then
        return self:refuseMove()
    else
        return self:acceptMove()
    end
end

Test runs so far. Now the useful check:

        _:test("TileArbiter: monster can step on dead monster", 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 m1t = Tile:room(9,10)
            local monsterRes = Monster(m1t, runner)
            local m2t = Tile:room(10,10)
            local monsterMov = Monster(m2t, runner)
            local arb = TileArbiter(monsterRes, monsterMov)
            local moveTo = arb:moveTo()
            _:expect(moveTo).is(m2t)
            monsterRes:die()
            local moveTo = arb:moveTo()
            _:expect(moveTo).is(m1t)
        end)

And it runs. Our feature works. That was easier than I expected: I had forgotten how the TA managed the movement and thought we might have to do something more intricate. I love it when things go better than I expect, probably because it’s so rare.

It’s still only 0845 on a surprisingly white Christmas, so I can do a bit more before the household awakes. What would be just the right size?

I know something that I think isn’t the right size: battle. Let’s think about how we might do battle. Thinking is good if you don’t over do it.

Battle

In the fullness of time, our entities want to have attributes like strength, health, and possibly more, such as armor class and power weapons and such. (But much about armor class and weapons can be subsumed into health and strength. Perhaps not all.)

The current pink ghost is our only monster and it can kill the princess with one blow, and she can kill it with one blow. This is bad for the princess, and she’s my favorite. Plus there will be much more serious monsters if this ever becomes a real game.

Right now, whichever entity tries to move into another’s square, they have “initiative”, which basically means they can strike first. And they do:

function Player:startActionWithMonster(aMonster)
    aMonster:die()
end

function Monster:startActionWithPlayer(aPlayer)
    aPlayer:die()
end

This is, um, abrupt, and rather final.

What one might like would be for there to be a series of attacks, she hits it, it hits her back, until one of them dies or runs away. (Running away might well be a desirable thing to do.)

And while presently they fight their brief battle on the same square, if we keep them separate, we might be able to animate some effects during the battle, making it a bit more interesting. We’re definitely looking into the future, but when we’re planning a product, we consider everything … and then decide what small slice of everything to undertake.

What if the sequence was like this. The mover strikes at the resident with some randomized damage value based on strength and whatever. Maybe there’s a possibility of a miss. If they do hit, that much damage is done to the resident. If the resident dies, encounter over. If not, it strikes back. Repeat until death or escape.

We could imagine doing this with a long series of messages back and forth between mover and resident. But if mover hits resident and resident hits mover and mover hits resident again … are we now three levels down in calls? These objects aren’t fully autonomous, so we’d have to be careful.

Would their actions be controlled by a separate object, as we’re doing for motion into a tile? There could be an Encounter object that manages the battle.

Or could we have the two objects go autonomous for a while. Mover hits resident with damage X. That function just returns and the mover does nothing special. But the resident receives the hit, determines that it’s not dead yet, sets a timer and returns to the mover. The timer hits and the resident hits at the mover. And so on.

That’s tempting, but I fear that it’s too fancy. Would the entities actually “know” they were in a battle? Or would the tween just fire and the monster is like “oh, I guess I’ll hit someone”. It would at least have to know what tile to attack.

Hmm, which it could do by trying to move to it, which would give it initiative, cause it to hit at the player …

I can see how that might work … for example, if the monster doesn’t go down, the player backs away from the attack tile, avoiding the monster’s hit, the monster enters that tile and the player attacks again. Could work.

I’m a bit concerned about doing it with tweens, however, as they’ve shown themselves to cause weird things to happen during testing. And if we control the hit back with a tween, it’ll make testing this behavior really difficult.

Oh No

This may call for a test double. Imagine that the flow of an attacked entity goes like this:

  1. Remember tile where attack comes from. (Somehow.)
  2. Subtract hit strength from my strength. Die if <= 0.
  3. Set a timer for some short period, 0.5 seconds.
  4. When timer hits, try to move to attack tile, triggering:
  5. Hit attacker with random strength …

So far, I’ve avoided having any kind of special timer: I just do tween.delay when I want something to happen in the near future. So far, that’s just moving and animating the monsters.

But what if we had some kind of tiny timey wimey object that internally used tween to call us back? And what if we replaced that object, during testing, with one that we could just trigger from the test. We could then write a test that advanced time to its own liking.

The replacement timey wimey object, if we were to go that way, would be a “test double”. The wikipedia article is a pretty good starting reference for the idea.

This is all a bit more work than I want to do this morning, but I’m starting to think it could be a decent way to progress, to drive the battle, after the initial attack, by the other participants just trying to move onto the opponent’s tile. This wouldn’t support ranged attacks, at least not directly, but that’s for another day entirely.

Let’s make a small change in the direction of this idea. Instead of the start action methods sending die, let’s have them send a damage message, including a damage amount, to one day be random, and the tile the attacker is on, for the opponent to remember.

function Monster:startActionWithPlayer(aPlayer)
    aPlayer:damageFrom(self.tile,1)
end

function Monster:damageFrom(aTile,amount)
    self:die()
end

And the same with Player. Should make no difference, except that I think I want movement to be refused if the other guy is alive.

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

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

Now I suppose some tests may fail, we’ll see. And then I want to try it in game play.

function Player:isAlive()
    return self.alive
end

That was needed … And the tests run. Now to hunt for a monster. Well, I think it’s working, but I can’t really tell, because TileArbiter:moveTo does the action before checking the move, and the resident is already dead by then. We’re going to need to give monsters hit points to make this work.

function Monster:init(tile, runner)
    self.alive = true
    self.tile = tile
    self.tile:addContents(self)
    self.runner = runner
    self.standing = asset.builtin.Platformer_Art.Monster_Standing
    self.moving = asset.builtin.Platformer_Art.Monster_Moving
    self.dead = asset.builtin.Platformer_Art.Monster_Squished
    self.sprite1 = self.standing
    self.sprite2 = self.dead
    self.swap = 0
    self.move = 0
    self.hitPoints = 2

function Monster:damageFrom(aTile,amount)
    self.hitPoints = self.hitPoints - amount
    if self.hitPoints <= 0 then
        self:die()
    end
end

This may still be hard to see. I’ll try to stay two steps away from the monster except to run at it to attack and then run back. I had to make the princess invulnerable to be sure, but it does work. I have to try twice to step onto the monster tile. The first time I see no effect and can’t get in. The second time, the monster dies and I do get in.

Have I even committed yet this morning? No. What a dolt.

Commit: monsters can step on dead monsters. Monsters have two hit points.

Now, in a spirit of fairness, but not much, let’s give the princess five hit points.

function Player:init(tile, runner)
    self.alive = true
    self.tile = tile
    self.tile:illuminate()
    self.tile:addContents(self)
    self.runner = runner
    self.keys = 0
    self.hitPoints = 5
end

-- Instance Methods

function Player:damageFrom(aTile,amount)
    self.hitPoints = self.hitPoints - amount
    if self.hitPoints <= 0 then
        self:die()
    end
end

That seems to work. We do need some kind of indication of a strike, don’t we? Let’s add sound.

function Monster:damageFrom(aTile,amount)
    sound(asset.downloaded.A_Hero_s_Quest.Monster_Hit_1)
    self.hitPoints = self.hitPoints - amount
    if self.hitPoints <= 0 then
        sound(asset.downloaded.A_Hero_s_Quest.Monster_Die_1)
        self:die()
    end
end

function Player:damageFrom(aTile,amount)
    if not self:isAlive() then return end
    sound(asset.downloaded.A_Hero_s_Quest.Hurt_1, 1, 1.5)
    self.hitPoints = self.hitPoints - amount
    if self.hitPoints <= 0 then
        sound(asset.downloaded.A_Hero_s_Quest.Hurt_3,1,1.5)
        self:die()
    end
end

That works nicely:

princess dies

monster dies

That should do for today. I’l quickly sum up and get out of here. Commit: Hit points and sounds.

Summing Up

These changes have done in quite nicely. The TileArbiter is doing its job so far, and I believe that extending what we have now to a battle may be easy enough. In fact, as I think about it now with my head clear … the monster already attacks when it can. We don’t need to do anything special to make that happen, so perhaps the battle idea is moot. And the princess may find that her best move is to attack and jump back, which she can do manually as well.

We may have nearly enough battle logic with hit points. A few more monsters with different powers will let us exercise the objects and see what else we need.

For now, the game is actually a bit better.

Next time, I think we need to deal with chests and their contents.

See you then!


D2.zip