Maybe give the monsters a bit of volition. Maybe place them randomly. Maybe some screen controls. Who knows, I just write this stuff.

In the “Simple Dungeon” sample program, which I’m kind of echoing, the monsters, treasures, exits, and player are placed randomly in the dungeon. The monsters, as far as I’ve examined them, just always try to move toward the player. This does mean that they can get trapped. We can imagine things to do about that, including having them ask to be teleported somewhere else. Monsters are like that.

For now, let’s have them move toward the player if they can. I guess to make this interesting, we’d better place them randomly, rather than right next to the avatar as they are now.

Let’s write “randomRoomTile” on GameRunner. I don’t think I need to TDD this. Let’s see if I am mistaken.

function GameRunner:randomRoomTile()
    while true do
        tx,ty = math.random(1, self.tileCountX), math.random(1,self.tileCountY)
        local tile = self:getTile(tx,ty)
        if tile.kind == "room" then return tile end
    end
end

The only thing that could possibly go wrong here would be that it would loop forever, never finding a room tile. That’s not going to happen. A belt-and-suspenders approach might put a finite limit on the loop with an error at the end. I’m good with this. I think I’ll create a few monsters at random locations and see what happens.

function GameRunner:createMonsters(n)
    self.monsters = {}
    for i = 1,n or 1 do
        local t = self:randomRoomTile()
        table.insert(self.monsters, Monster(t.x,t.y,self))
    end
end

I’ll call it with 5.

five

They’re hard to see, but they’re in there. Commit: five monsters placed randomly.

Glancing at the code, I’m reminded of the time hackery I did yesterday, using os.time etc. A chat on the Codea forum reminded me of the tween delay function, which seems like a much better way to manage this kind of thing. Each object that wants timing can just manage its own delays.

In addition, with five ghosts wandering, I was able to notice that they all change their animation states at the same time. This makes sense, as they were all created within zilliseconds of each other. So let’s randomize the time they use just a bit.

First, I’ll remove the time stuff from GameRunner. Nothing to see here, just a bunch of removed code. Ah, here’s a diff from Working Copy:

diff

Now, when a monster inits, he wants to have a timer for his animation, and a timer for when to move. Presently they have this code:

function Monster:time(aTime)
    self:moveIfTime()
    self:animateIfTime()
end

function Monster:moveIfTime()
    self.move = self.move + 1
    if self.move >= 4 then
        self.move = 0
        local moves = { {x=-1,y=0}, {x=0,y=1}, {x=0,y=-1}, {x=1,y=0}}
        local move = moves[math.random(1,4)]
        local nx, ny = self.tileX + move.x, self.tileY + move.y
        self.runner:moveMeIfPossible(self, nx,ny)
    end
end

function Monster:animateIfTime()
    self.swap = self.swap + 1
    if self.swap == 2 then
        self.swap = 0
        self.sprite1,self.sprite2 = self.sprite2,self.sprite1
    end
end

We’ll remove the time function, since no one calls it, and rename the other two to … to what. We already have a move method in our external protocol. That’s what GameRunner calls if a requested move is approved. So we can’t rename moveIfTime to move, much as we might like to. Maybe choose. OK, we’ll go with that.

function Monster:init(tileX,tileY, runner)
    self.tileX = tileX
    self.tileY = tileY
    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:timer(self.chooseAnimation, 0.5, 0.05)
    self:timer(self.chooseMove, 1.0, 0.5)
end

function Monster:timer(action, time, deltaTime)
    local t = time + math.random()*deltaTime
    tween.delay(t, action, self)
end

function Monster:chooseAnimation()
    self.sprite1,self.sprite2 = self.sprite2,self.sprite1
    self:timer(self.chooseAnimation, 0.5, 0.05)
end

function Monster:chooseMove()
    local moves = { {x=-1,y=0}, {x=0,y=1}, {x=0,y=-1}, {x=1,y=0}}
    local move = moves[math.random(1,4)]
    local nx, ny = self.tileX + move.x, self.tileY + move.y
    self.runner:moveMeIfPossible(self, nx,ny)
    self:timer(self.chooseMove, 1.0, 0.5)
end

I think I’ll rename timer to setTimer. That seems better.

There’s duplication of those calls to setTimer and we do want them always to be the same. Two new little methods:

function Monster:init(tileX,tileY, runner)
    self.tileX = tileX
    self.tileY = tileY
    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:setAnimationTimer()
    self:setMotionTimer()
end

function Monster:setAnimationTimer()
    self:setTimer(self.chooseAnimation, 0.5, 0.05)
end

function Monster:setMotionTimer()
    self:setTimer(self.chooseMove, 1.0, 0.5)
end

function Monster:setTimer(action, time, deltaTime)
    local t = time + math.random()*deltaTime
    tween.delay(t, action, self)
end

function Monster:chooseAnimation()
    self.sprite1,self.sprite2 = self.sprite2,self.sprite1
    self:setAnimationTimer()
end

function Monster:chooseMove()
    local moves = { {x=-1,y=0}, {x=0,y=1}, {x=0,y=-1}, {x=1,y=0}}
    local move = moves[math.random(1,4)]
    local nx, ny = self.tileX + move.x, self.tileY + move.y
    self.runner:moveMeIfPossible(self, nx,ny)
    self:setMotionTimer()
end

OK, that’s nice. No duplication to speak of. Commit: monsters use random tweens for animation and motion.

Now what about giving these guys some purposeful motion? We can ask GameRunner “which way to the princess” and work from that. Let’s look at chooseMove and see what we’d really like.

It seems to me that there are two values of interest in the answer to “which way is the princess”, namely whether we should go up or down, or left or right. (We can’t do both. Why not? Maybe we should allow that for monsters. Maybe later.)

Let’s leave it that there are at most two possible moves toward the princess, toward her in y, and toward her in x. Plus or minue one in either case. We will select which to try, randomly.

I notice that SimpleDungeon doesn’t move the monsters unless they are close to the player. That’s an interesting decision. How about if we move randomly unless we are fairly close to the player and then move toward them?

Let’s code this by intention. First I’ll refactor:

function Monster:chooseMove()
    local moves = { {x=-1,y=0}, {x=0,y=1}, {x=0,y=-1}, {x=1,y=0}}
    local move = moves[math.random(1,4)]
    local nx, ny = self.tileX + move.x, self.tileY + move.y
    self.runner:moveMeIfPossible(self, nx,ny)
    self:setMotionTimer()
end
function Monster:chooseMove()
    local nx,ny = proposeRandomMove()
    self.runner:moveMeIfPossible(self, nx,ny)
    self:setMotionTimer()
end

function Monster:proposeRandomMove()
    local moves = { {x=-1,y=0}, {x=0,y=1}, {x=0,y=-1}, {x=1,y=0}}
    local move = moves[math.random(1,4)]
    return self.tileX + move.x, self.tileY + move.y
end

Now, I guess, a simple if statement for now:

function Monster:chooseMove()
    local nx,ny
    if self.runner:playerDistanceXY(self.tileX,self.tileY) <= 10 then
        nx,ny = proposeMoveTowardAvatar()
    else
        nx,ny = proposeRandomMove()
    end
    self.runner:moveMeIfPossible(self, nx,ny)
    self:setMotionTimer()
end

Now getting close to needing to do some work here …

function Monster:proposeMoveTowardAvatar()
    local dx,dy = runner:playerDirection(self.tileX, self.tileY)
    if math.random() < 0.5 then
        return 0,dy
    else 
        return dx,0
    end
end

I have in mind here that player direction returns -1,0,+1 according to where the player is relative to the provided x and y. So …

function GameRunner:playerDirection(x,y)
    tx,ty = player:position()
    return sign(tx), sign(ty)
end

function sign(x)
    return (x < 0 and -1) or (x == 0 and 0) or (x > 0 and 1)
end

I have high hopes for this. How will my hopes be dashed?

Monster:28: attempt to call a nil value (global 'proposeRandomMove')
stack traceback:
	Monster:28: in field 'callback'
	...in pairs(tweens) do
    c = c + 1
  end
  return c
end

This is inside the tween callbac I probably forgot a self somewhere. Indeed:

function Monster:chooseMove()
    local nx,ny
    if self.runner:playerDistanceXY(self.tileX,self.tileY) <= 10 then
        nx,ny = proposeMoveTowardAvatar()
    else
        nx,ny = proposeRandomMove()
    end
    self.runner:moveMeIfPossible(self, nx,ny)
    self:setMotionTimer()
end

Why didn’t you mention that? Anyway …

function Monster:chooseMove()
    local nx,ny
    if self.runner:playerDistanceXY(self.tileX,self.tileY) <= 10 then
        nx,ny = self:proposeMoveTowardAvatar()
    else
        nx,ny = self:proposeRandomMove()
    end
    self.runner:moveMeIfPossible(self, nx,ny)
    self:setMotionTimer()
end
GameRunner:141: attempt to index a nil value (global 'player')
stack traceback:
	GameRunner:141: in method 'playerDirection'
	Monster:47: in method 'proposeMoveTowardAvatar'
	Monster:26: in field 'callback'
	...in pairs(tweens) do
    c = c + 1
  end
  return c
end

That’ll be wrong.

function GameRunner:playerDirection(x,y)
    tx,ty = player:position()
    return sign(tx), sign(ty)
end

What language do I think I’m using here, anyway. Needs self:

function GameRunner:playerDirection(x,y)
    tx,ty = self.player:position()
    return sign(tx), sign(ty)
end

Damn. It’s called avatar not player. Naming inconsistency. Fix now, but we should normalize this.

function GameRunner:playerDirection(x,y)
    tx,ty = self.avatar:position()
    return sign(tx), sign(ty)
end

These errors are happening after I move the princess near a ghostie, so they are telling me that we’re trying to do the move toward thing. So that’s good news. Ish.

However:

two ghosts

I got started with these two ghosts near me, and they are not moving. Something isn’t right here. I think we’d like to test this proposal code. Past time, isn’t it?

Looking at this:

function Monster:proposeMoveTowardAvatar()
    local dx,dy = runner:playerDirection(self.tileX, self.tileY)
    if math.random() < 0.5 then
        return 0,dy
    else 
        return dx,0
    end
end

It seems to me that it can only fail if it gets zero dx and dy. I decided to print dx and dy, and discovered to my surprise that they are always 1,1. Which really says to me that … Ah … this is wrong:

function Monster:chooseMove()
    local nx,ny
    if self.runner:playerDistanceXY(self.tileX,self.tileY) <= 10 then
        nx,ny = self:proposeMoveTowardAvatar()
    else
        nx,ny = self:proposeRandomMove()
    end
    self.runner:moveMeIfPossible(self, nx,ny)
    self:setMotionTimer()
end

We’re trying to move to 1,1. We (that is to say, I) forgot to add the proposed move to the current position.

function Monster:chooseMove()
    local nx,ny
    if self.runner:playerDistanceXY(self.tileX,self.tileY) <= 10 then
        nx,ny = self:proposeMoveTowardAvatar()
    else
        nx,ny = self:proposeRandomMove()
    end
    self.runner:moveMeIfPossible(self, self.tileX+nx, self.tileY+ny)
    self:setMotionTimer()
end

That explains why we didn’t move. But why was that function returning 1,1 all the time. We do want a test, still.

        _:test("player direction", function()
            local dx,dy
            local runner = GameRunner()
            local player = Player(100,100)
            runner.avatar = player
            dx,dy = runner:playerDirection(101,101)
            _:expect(dx).is(-1)
            _:expect(dy).is(-1)
        end)

Tests failed but also got this odd message:

Main:39: attempt to index a nil value (global 'runner')
stack traceback:
	Main:39: in function 'draw'
function draw()
    pushMatrix()
    if CodeaUnit then showCodeaUnitTests() end
    if DisplayToggle then
        local x,y = Runner.avatar:tileCoordinates()
        local gx,gy = x*Runner.tileSize,y*runner.tileSize
        focus(gx,gy, 1)
    else
        scale(0.25)
    end
    Runner:draw()
    popMatrix()
end

Lower case r. How did that ever work? It turns out that I forgot a local runner in a test, so the test was defining a global runner, and I fixed that while writing this new test. Fascinating.

Now I get this:

Monster:47: attempt to index a nil value (global 'runner')
stack traceback:
	Monster:47: in method 'proposeMoveTowardAvatar'
	Monster:26: in field 'callback'
	...in pairs(tweens) do
    c = c + 1
  end
  return c
end

Someone else with the same mistake?

function Monster:proposeMoveTowardAvatar()
    local dx,dy = runner:playerDirection(self.tileX, self.tileY)
    print(dx,dy)
    if math.random() < 0.5 then
        return 0,dy
    else 
        return dx,0
    end
end

Yep. Fascinating again. Now about that test:

9: player direction  -- Actual: 1, Expected: -1

Did I forget the subtract? I bet I did.

function GameRunner:playerDirection(x,y)
    tx,ty = self.avatar:position()
    return sign(tx), sign(ty)
end

Sure did. Let’s see. Which way should the subtract go? tx - x.

function GameRunner:playerDirection(x,y)
    tx,ty = self.avatar:position()
    return sign(tx-x), sign(ty-y)
end

Test runs now, and I couldn’t resist a live action test:

chase

The test surely isn’t complete. I’ll add a couple more checks just so it doesn’t look odd.

        _:test("player direction", function()
            local dx,dy
            local runner = GameRunner()
            local player = Player(100,100)
            runner.avatar = player
            dx,dy = runner:playerDirection(101,101)
            _:expect(dx).is(-1)
            _:expect(dy).is(-1)
            dx,dy = runner:playerDirection(98,98)
            _:expect(dx).is(1)
            _:expect(dy).is(1)
        end)

That’ll suffice for my porpoises, they’re pretty easy-going.

Commit: monsters chase princess within 10 cells.

Now what about that naming thing? The class is called Player, and the variable is named avatar. Many methods refer to player. None refer to avatar. I’ll change all reverences to avatar to player. There’s one in main, a few in test, and a bunch in GameRunner. Not worth displaying the code.

Program runs fine, tests are green. Commit: rename avatar to player throughout.

I’m not sure about these ghosts. The bastards surrounded me:

surrounded

Summing Up

We’re right at two hours, so thats good. Despite more than my quota of silly mistakes, we got random monster placement, converted timing to use tweens, taught the monsters to chase the princess, and improved the naming just a bit here and there.

Not bad at all.

What about tests? I finally did that one for playerDirection and it was worth doing, if only because it told me clearly that something was wrong and caused me to think about whether I’d subtracted the values. I hadn’t.

Would other tests have helped? I really don’t think they would have, but I could be wrong. If you can see where a test would have helped, tweet me up.

See you next time!


D2.zip