Top o’ the morning’ to ya, and I think we’ll be starting on a set-piece today. Unless I change my mind. Also, some ranting about Definition of Ready.

Yes, I am half-Irish, on my mother’s side. That said, most of the green clothing I have looks more black than green. I have a good reason for that: I’m a Winter, and black is my color.

I was thinking this morning, while the cat sat on me, about what to do with the new path-finding capability. I’d already thought of there being monsters that would lead you to things, such as the WayDown. So now, I’ve got a “big” story in mind:

Near the WayDown, position a few guardian monsters, perhaps from the next level. Those monsters never leave the area of the WayDown. Perhaps they will battle. Maybe they just block the player from the WayDown. (If so, how does she get past them?)

The WayDown requires a key. Once the player finds a key, one guardian monster occasionally appears near the player. This monster never comes closer than (say) 5, nor moves further away than (say) 10. Its main purpose is to lead the player to the WayDown room and the final confrontation with the guardians.

The monster needs enough intelligence to carry out this duty without doing anything really strange like trampling the player. Details will need to be worked out.

Over the next while, we’ll be working toward this story. At this moment, I’m not sure what order we’ll proceed in, nor quite how any of this will be done. That’s just fine: why should I know exactly how to do some ill-defined thing I just thought of an hour ago?

Definition of Ready

I must digress. A common “idea” in Scrum is the “Definition of Ready”, an agreement between Product Owner and developers as to when a story is well-enough described to be ready to be brought to Sprint Planning. Frequent elements of this “definition” are that acceptance tests must be defined or ready, and that the Product Owner has answered, or can answer, any questions the team may have about the story.

I think this notion is weak. First of all, it turns out to be used quite often as a weapon for the developers to resist doing things. Anything they don’t want to do, they pull out the DoR and start playing Story Lawyer with the PO. This is counter-productive. We’re supposed to collaborate, not battle.

Second, the DoR practice tends to nail down details too soon, and to generate rigid stories that are too large. In order to be “ready”, the Product Owner is induced to go into great detail about the story, and then, whether all the details are good or not, we’re pretty much stuck with it.

So you’d be correct to think that no reasonable Definition of Ready would accept the story above. That’s probably because there is no such thing as a reasonable Definition of Ready.

What do we do instead? We do what we’re going to do right here: we collaborate as a group to figure out what’s needed and how to do it.

But I digress …

Set Piece

Given what we have, we can envision a room with the WayDown in it, and a few monsters roaming around the room. They’ll need a new kind of MonsterStrategy. It doesn’t sound too complicated, and is probably similar to the Calm strategy, except that instead of milling around near the player they mill around near the WayDown.

We can certainly envision another monster who can appear near the player and then lead to the WayDown room, and there should be no problem causing that monster to change strategy to become a regular guardian, or perhaps just enter the WayDown to go on break.

If we want the guardians to block the player, that would be another strategy, basically “move between the WayDown and the player”, although we’ll have to figure out details.

This will be our first “set piece”, unless along the way we come up with something interesting.

I guess the obvious place to start, since we’ve just been working on it, is a monster that walks from somewhere to the WayDown. We can just randomly produce that monster, or produce it near the player, and let it go.

Another reason for working on this is that our current test feature for the pathfinding needs to come out, and in building it, we’ve had to write some unfortunate code.

So let’s start there.

Clean Up the Pathfinding Interface

When the player touches the “Flee” button, we lay down Health Loots along the path from the player to the WayDown. We do that like this:

function Player:flee()
    local dungeon = self.runner:getDungeon()
    local map = Map(dungeon)
    local me = self:getTile():pos()
    local wd = self.runner.wayDown.tile:pos()
    map:start(wd.x,wd.y)
    map:run()
    local path = map:cell(me.x,me.y):path()
    for i,c in ipairs(path) do
        self:markTile(c)
    end
end

function Player:markTile(mapCell)
    local pos = mapCell:posVec()
    local dung = self.runner:getDungeon()
    local tile = dung:getTile(pos)
    print("markTile", pos,dung,tile)
    tile:addDeferredContents(Loot(tile, "Health", 1,1))
end

This code doesn’t tell a very coherent story. Let’s try to express it a bit more clearly, first in English.

Our purpose is to get a path from somewhere (the player location) to somewhere else (the WayDown location). To do that, we:

  1. Create a Map of the current dungeon.
  2. Map the dungeon with paths to the WayDown.
  3. Pull out the path to the player location.
  4. (Mark the path’s tiles.)

Note that what Map:run does is to link every cell in the Map back to the point we call “start”. (We should probably call it “stop” or “target”. After run has completed, every MapCell has a link to the MapCell you should move to if you want to get to the target. We just pull out one such path by following those links.

So what we really want is for someone to give us a path, an array of Tiles. It’s possible that we should settle for an array of steps. Without looking, I think we’d prefer tiles.

As I think about this further, maybe we don’t need to create the path in the usual case at all. Maybe we’d really like to retain the whole Map, with the links all in there.

Think about it. If we have a map to the WayDown, any time a monster wants to move toward the WayDown, it could just access the map at the point that corresponds to its current location, and the Map could give back the coordinates, or the Tile, to move to. The monster could try to move to that Tile. If it succeeds, repeat. If it fails for some reason, it could move randomly and try again next time.

Yes, I like that. We’ll retain the path ability: we might want to mark something. But let’s assume that having created a Map to somewhere, we can hold on to it and use it repeatedly. We can always refresh it if we need to.

So let’s recast the Flee code in those terms. That is, let’s create the map, changing the protocol from start to target, and using the Map to give us the tile to populate.

Adjusting the Protocol

I think I’d like the map to think in terms of Tiles, because that’s what the user code uses.

My first cut is this:

function Player:flee()
    local dungeon = self.runner:getDungeon()
    local map = Map(dungeon)
    map:target(self.runner.wayDown.tile)
    map:run()
    local next = self:getTile()
    while next do
        next = map:nextAfter(next)
        if next then self:markTile(next) end
    end
end

I’ve posited a method target that accepts a tile, and a method nextAfter that also accepts a tile.

I’ll need to build these in Map and MapCell. Let’s try to TDD these. This is enough to start:

        _:test("target accepts tile", function()
            local dungeonClass = FakeMapDungeon
            local tiles = FakeMapTile:create(50,50)
            local dungeon = dungeonClass(tiles)
            local map = Map(dungeon)
            local t = FakeMapTile(10,10)
            map:target(t)
            map:run()
        end)

I’m going to change the start internally to something else, perhaps targetXY, since it now accepts and x and y.

function Map:target(tile)
    self:targetXY(unpack(tile:pos()))
end

function Map:targetXY(x,y)
    self.q = {}
    local c = self:cell(x,y)
    c.distance = 0
    c.parent = nil
    self:addQueue(c)
end

This will require me to change all the start references in the tests. Consider it done.

I also have to implement this:

function FakeMapTile:pos()
    return vec2(x,y)
end

Can’t call unpack. Lua 5.4 has forced us to quality if. I think we can send it, however.

function Map:target(tile)
    self:targetXY(tile:pos():unpack())
end

Now I get this:

8: target accepts tile -- PathFinder:158: attempt to index a nil value (field '?')

That’s here:

function Map:cell(x,y)
    return self.cells[x][y]
end

The bug is this:

function FakeMapTile:pos()
    return vec2(self.x,self.y)
end

Forgot the selfs.

Now the new test runs. Of course it doesn’t assert anything yet.

            map:run()
            t = FakeMapTile(11,10)
            local g = map:nextTile(t)
            local pos = g:pos()
            _:expect(pos).is(vec2(10,10))

I figure the adjacent cell should point to the target.

8: target accepts tile -- PathFinder:127: attempt to call a nil value (method 'nextTile')

As expected.

function Map:nextTile(tile)
    local cell = self:getCellFromTile(tile)
    local prev = cell.parent
    if not prev then return nil end
    return self.dungeon:getTile(vec2(prev.x,prev.y))
end

function Map:getCellFromTile(tile)
    local pos = tile:pos()
    return self:getCellFromPosition(pos)
end

function Map:getCellFromPosition(pos)
    return self:cell(pos.x,pos.y)
end

And I have to extend the FakeMapDungeon:

function FakeMapDungeon:getTile(pos)
    return self.tiles[pos.x][pos.y]
end

The test runs.

If things are as they should be, then the Flee button should also work.

Well, it would if I had used the same method name:

function Map:nextAfter(tile)
    local cell = self:getCellFromTile(tile)
    local prev = cell.parent
    if not prev then return nil end
    return self.dungeon:getTile(vec2(prev.x,prev.y))
end

And if I had fixed MarkTile to expect a tile.

function Player:markTile(tile)
    tile:addDeferredContents(Loot(tile, "Health", 1,1))
end

And now the path appears again. Now let’s look again at how we set up the Map and improve it more.

function Player:flee()
    local dungeon = self.runner:getDungeon()
    local map = Map(dungeon)
    map:target(self.runner.wayDown.tile)
    map:run()
    local next = self:getTile()
    while next do
        next = map:nextAfter(next)
        if next then self:markTile(next) end
    end
end

It seems that one never wants anything but a Map to a given target. So let’s create what we want. And since the GameRunner is the owner of the dungeon, we should ask it to give us the map.

And since it knows where the WayDown is, let’s just say this:

function Player:flee()
    local map = self.runner:mapToWayDown()
    local next = self:getTile()
    while next do
        next = map:nextAfter(next)
        if next then self:markTile(next) end
    end
end
function GameRunner:mapToWayDown()
    local map = Map(self:getDungeon())
    map:target(self.wayDown.tile)
    map:run()
    return map
end

Simple enough. But why do we bother to say run? There’s nothing else to do. Change Map.

function Map:target(tile)
    self:targetXY(tile:pos():unpack())
    self:run()
end

Now we have this:

function GameRunner:mapToWayDown()
    local map = Map(self:getDungeon())
    map:target(self.wayDown.tile)
    return map
end

Why not just pass the target when we create?

function GameRunner:mapToWayDown()
    return Map(self:getDungeon(), self.wayDown.tile)
end

We can do this.

function Map:init(dungeon, tile)
    self.dungeon = dungeon
    self:initCells()
    self.visited = false
    if tile then self:target(tile) end
end

This is nice. From the outside, we speak in terms of tiles, which are the core object in the dungeon. Inside, we deal with the conversions between our MapCells and tiles. And we have one-stop shopping to get the map we want.

We’ll follow this pattern to provide other paths as needed, such as paths to treasures or paths to allow monsters to seek out the player or the like.

Nice. Commit: improve interface to Map for pathfinding.

What Shall We Do Now?

Let’s do a new monster strategy, one that keeps the monster within a few tiles of the WayDown, or other defined tile.

Let’s review our strategy code.

function CalmMonsterStrategy:execute(dungeon)
    local method = self:selectMove(self.monster:manhattanDistanceFromPlayer())
    self.monster[method](self.monster, dungeon)
end

function CalmMonsterStrategy:selectMove(range)
    if range > 10 then
        return "basicMoveRandomly"
    elseif range >= 4 then
        return "basicMoveTowardPlayer"
    elseif range == 3 then
        return "basicMaintainRangeToPlayer"
    elseif range == 2 then
        return "basicMoveAwayFromPlayer"
    elseif range == 1 then
        return "basicMoveTowardPlayer"
    else
        return "basicMoveAwayFromPlayer"
    end
end

The monsters know how to do those basic moves:

function Monster:basicMaintainRangeToPlayer(dungeon)
    local tiles = dungeon:availableTilesSameDistanceFromPlayer(self.tile)
    self.tile = self.tile:validateMoveTo(self,tiles[math.random(1,#tiles)])
end

function Monster:basicMoveAwayFromPlayer(dungeon)
    local tiles = dungeon:availableTilesFurtherFromPlayer(self.tile)
    self.tile = self.tile:validateMoveTo(self,tiles[math.random(1,#tiles)])
end

function Monster:basicMoveRandomly(ignoredDungeon)
    local moves = {vec2(-1,0), vec2(0,1), vec2(0,-1), vec2(1,0)}
    local move = moves[math.random(1,4)]
    self.tile = self.tile:legalNeighbor(self,move)
end

function Monster:basicMoveTowardPlayer(dungeon)
    local tiles = dungeon:availableTilesCloserToPlayer(self.tile)
    self.tile = self.tile:validateMoveTo(self,tiles[math.random(1,#tiles)])
end

We can foresee that we’ll want one or two more moves here, toward or away from a tile. No sense writing them until we need them.

So what’s the strategy? Let’s move no nearer than 3 to the WayDown, and no further away than 6. We’ll go for a bit of generalization and use a general tile, not specifically the WayDown.

This is arguably a poor idea. It might be better to do the simpler thing and then generalize, but I think in this case we’re fine.

I basically just typed this in:

-- Hanging Out Monster

HangoutMonsterStrategy = class()

function HangoutMonsterStrategy:init(monster, tile)
    self.monster = monster
    self.tile = tile
    self.min = 3
    self.max = 6
end

function HangoutMonsterStrategy:execute(dungeon)
    local method = self:selectMove(self.monster:manhattanDistanceFromTile(tile))
    self.monster[method](self.monster, dungeon, tile)
end

function HangoutMonsterStrategy:selectMove(range)
    if range < min  then
        return "basicMoveAwayFromTile"
    elseif self.min <= range  and range <= self.max then
        return "basicMoveRandomly"
    else
        return "basicMoveTowardTile"
    end
end

I suppose I should at least test this selection code, I usually get it wrong.

        _:test("Hangout strategy tests", function()
            local method
            local strat = HangoutMonsterStrategy(nil,nil)
            method = strat:selectMove(7)
            _:expect(method).is("basicMoveTowardTile")
            method = strat:selectMove(6)
            _:expect(method).is("basicMoveRandomly")
            method = strat:selectMove(5)
            _:expect(method).is("basicMoveRandomly")
            method = strat:selectMove(4)
            _:expect(method).is("basicMoveRandomly")
            method = strat:selectMove(3)
            _:expect(method).is("basicMoveRandomly")
            method = strat:selectMove(2)
            _:expect(method).is("basicMoveAwayFromTile")
        end)

There you go, and the test runs.

Now, however, we need the two new movers.

We’ll begin by parroting this one:

function Monster:basicMoveAwayFromPlayer(dungeon)
    local tiles = dungeon:availableTilesFurtherFromPlayer(self.tile)
    self.tile = self.tile:validateMoveTo(self,tiles[math.random(1,#tiles)])
end

Let’s see what Dungeon knows. All the various distance methods come down to this one:

function Dungeon:availableTilesAtDistanceFromPlayer(aTile, desiredDistance)
    local pos = aTile:pos()
    local result = {}
    for i, delta in ipairs(self:neighborOffsets()) do
        local t = self:getTile(pos + delta)
        local d = self:manhattanToPlayer(t)
        if d == desiredDistance and (t:isRoom() or t:isEdge()) then
            table.insert(result, t)
        end
    end
    if #result == 0 then
        result = {aTile}
    end
    return result
end

We’d like to extend this to allow provision of a comparison tile other than the player.

Let’s refactor:

function Dungeon:availableTilesAtDistanceFromPlayer(startingTile, desiredDistance)
    local pos = startingTile:pos()
    local playerTile = self:playerTile()
    local result = {}
    for i, delta in ipairs(self:neighborOffsets()) do
        local t = self:getTile(pos + delta)
        local d = self:manhattanBetween(t, playerTile)
        if d == desiredDistance and (t:isRoom() or t:isEdge()) then
            table.insert(result, t)
        end
    end
    if #result == 0 then
        result = {aTile}
    end
    return result
end

function Dungeon:manhattanBetween(tile1, tile2)
    return tile1:manhattanDistance(tile2)
end

function Dungeon:manhattanToPlayer(tile)
    return self:manhattanBetween(tile, self:playerTile())
end

This should be equivalent, and all seems in order. Now we can extract a method. First, reorder this:

function Dungeon:availableTilesAtDistanceFromPlayer(startingTile, desiredDistance)
    local playerTile = self:playerTile()
    local pos = startingTile:pos()
    local result = {}
    for i, delta in ipairs(self:neighborOffsets()) do
        local t = self:getTile(pos + delta)
        local d = self:manhattanBetween(t, playerTile)
        if d == desiredDistance and (t:isRoom() or t:isEdge()) then
            table.insert(result, t)
        end
    end
    if #result == 0 then
        result = {aTile}
    end
    return result
end

Now rename to keep my head on straight:

function Dungeon:availableTilesAtDistanceFromPlayer(startingTile, desiredDistance)
    local targetTile = self:playerTile()
    local pos = startingTile:pos()
    local result = {}
    for i, delta in ipairs(self:neighborOffsets()) do
        local t = self:getTile(pos + delta)
        local d = self:manhattanBetween(t, targetTile)
        if d == desiredDistance and (t:isRoom() or t:isEdge()) then
            table.insert(result, t)
        end
    end
    if #result == 0 then
        result = {aTile}
    end
    return result
end

I renamed playerTile to targetTile. Now extract method:

function Dungeon:availableTilesAtDistanceFromPlayer(startingTile, desiredDistance)
    local targetTile = self:playerTile()
    return self:availableTilesAtDistanceFromTile(startingTile, targetTile, desiredDistance)
end

function Dungeon:availableTilesAtDistanceFromTile(startingTile, targetTile, desiredDistance)
    local pos = startingTile:pos()
    local result = {}
    for i, delta in ipairs(self:neighborOffsets()) do
        local t = self:getTile(pos + delta)
        local d = self:manhattanBetween(t, targetTile)
        if d == desiredDistance and (t:isRoom() or t:isEdge()) then
            table.insert(result, t)
        end
    end
    if #result == 0 then
        result = {aTile}
    end
    return result
end

This should be a pure refactoring if I haven’t messed up. It appears that I have not messed up.

Now we can implement our desired new methods from the HangoutMonsterStrategy:

function HangoutMonsterStrategy:execute(dungeon)
    local method = self:selectMove(self.monster:manhattanDistanceFromTile(tile))
    self.monster[method](self.monster, dungeon, self.tile)
end

function HangoutMonsterStrategy:selectMove(range)
    if range < self.min  then
        return "basicMoveAwayFromTile"
    elseif self.min <= range  and range <= self.max then
        return "basicMoveRandomly"
    else
        return "basicMoveTowardTile"
    end
end

(I saw and fixed the missing self on tile in the execute above. I sure to make that error frequently.)

We’ll be passing in the dungeon and the tile to these new methods:

function Monster:basicMoveAwayFromTile(dungeon, tile)
    local tiles = dungeon:availableTilesFurtherFromTile(tile)
    self.tile = self.tile:validateMoveTo(self,tiles[math.random(1,#tiles)])
end

function Monster:basicMoveTowardTile(dungeon, tile)
    local tiles = dungeon:availableTilesCloserToTile(tile)
    self.tile = self.tile:validateMoveTo(self,tiles[math.random(1,#tiles)])
end

Now these new methods aren’t in Dungeon yet, so:

function Dungeon:availableTilesCloserToTile(tile1, tile2)
    local desiredDistance = self:manhattanBetween(tile1,tile2) - 1
    return self:availableTilesAtDistanceFromTile(tile1,tile2, desiredDistance)
end

function Dungeon:availableTilesFurtherFromTile(tile1, tile2)
    local desiredDistance = self:manhattanBetween(tile1,tile2) + 1
    return self:availableTilesAtDistanceFromTile(tile1,tile2, desiredDistance)
end

Now we have no actual tests for monsters running this strategy, so let’s create one near the WayDown.

Assessment

Lifting my head up for a moment from deep in the keyboard, this has been going on a while, and we’re not really done yet. Of course all the tests run and the game works, but this new strategy still isn’t in use.

I want to get to an actual in-game monster running this strategy asap. I could do a new one near the WayDown, or put one somewhere else.

Let’s create a monster with our new strategy, in Room 1, centered.

Grrr

After too long a time, I’ve failed to set up a test monster. They’re too tricky to set up in an ad-hoc fashion. I’d like to revert, but sadly it has been a long time since the last commit.

I’ve burned an hour since the “What Shall We Do Now”, and there’s a crash in the game.

Nothing for it but to debug for ages, or revert. One quick look to see if I can debug. The crash is very intermittent:

Monster:141: bad argument #1 to 'random' (interval is empty)
stack traceback:
	[C]: in function 'math.random'
	Monster:141: in field '?'
	MonsterStrategy:15: in method 'execute'
	Monster:234: in method 'executeMoveStrategy'
	Monster:185: in method 'chooseMove'
	Monsters:61: in method 'move'
	GameRunner:339: in method 'playerTurnComplete'
	Player:238: in method 'turnComplete'
	Player:177: in method 'keyPress'
	GameRunner:289: in method 'keyPress'
	Main:33: in function 'keyboard'

The picture was this:

two pink slimes

I had just tried to move right, toward the slime in the open area, when the program crashed. I don’t know which monster crashed, but since the one to the left had been following me, I suspect it’s the one on the right. I don’t know whether he’s calm or nasty.

However, I wish I had looked at the screen more carefully because we’re here:

function CalmMonsterStrategy:execute(dungeon)
    local method = self:selectMove(self.monster:manhattanDistanceFromPlayer())
    print("calm ", method)
    self.monster[method](self.monster, dungeon)
end

Another try suggests that the method is this:

calm 	basicMaintainRangeToPlayer

Ah. In this case, with the monster right there at the edge, there is no move he can make that maintains range. Let’s examine that code.

function Monster:basicMaintainRangeToPlayer(dungeon)
    local tiles = dungeon:availableTilesSameDistanceFromPlayer(self.tile)
    self.tile = self.tile:validateMoveTo(self,tiles[math.random(1,#tiles)])
end

Ha!

function Dungeon:availableTilesSameDistanceFromPlayer(aTile)
    local desiredDistance = self:manhattanToPlayer(aTile)
    return self:availableTilesAtDistanceFromPlayer(aTile, desiredDistance)
end

function Dungeon:availableTilesAtDistanceFromPlayer(startingTile, desiredDistance)
    local targetTile = self:playerTile()
    return self:availableTilesAtDistanceFromTile(startingTile, targetTile, desiredDistance)
end

function Dungeon:availableTilesAtDistanceFromTile(startingTile, targetTile, desiredDistance)
    local pos = startingTile:pos()
    local result = {}
    for i, delta in ipairs(self:neighborOffsets()) do
        local t = self:getTile(pos + delta)
        local d = self:manhattanBetween(t, targetTile)
        if d == desiredDistance and (t:isRoom() or t:isEdge()) then
            table.insert(result, t)
        end
    end
    if #result == 0 then
        result = {aTile}
    end
    return result
end

Note that result = {aTile}. Refactoring defect. there is no aTile. That should be startingTile.

function Dungeon:availableTilesAtDistanceFromTile(startingTile, targetTile, desiredDistance)
    local pos = startingTile:pos()
    local result = {}
    for i, delta in ipairs(self:neighborOffsets()) do
        local t = self:getTile(pos + delta)
        local d = self:manhattanBetween(t, targetTile)
        if d == desiredDistance and (t:isRoom() or t:isEdge()) then
            table.insert(result, t)
        end
    end
    if #result == 0 then
        result = {startingTile}
    end
    return result
end

I’m quite sure that will fix the problem, but there’s no easy test. Maybe I can try to trick a monster into the right hallway position.

Fairly comprehensive testing convinces me that the problem is fixed. It’s quite late, let’s sum up.

Summary

Today went well until it didn’t. I performed a refactoring that seemed correct but I had failed to rename one of the internal parameter references, which fetched Lua’s default nil in a low-probability case, resulting in a rare but certain crash.

There is a trick, fairly deep in the bag, that would let us modify Lua’s global table so as not to allow references to undefined terms. I’ve never tried it, but it might well have detected this error and given us a better diagnostic. I’ll at least make a note of the idea.

Beyond that, we’re going to encounter a small number of defects due to failed edits or simple typographical errors.

Now it is quite arguable that there should be a test for this case, since there is very clearly some custom code to handle it:

    if #result == 0 then
        result = {startingTile}
    end

I’m not sure that we even have tests directly aimed at these availableTiles... methods. Well, it turns out that we have one weak test:

        _:test("Dungeon helps monster moves", function()
            local tiles, x1,x2
            local dungeon = Runner:getDungeon()
            dungeon:setTestPlayerAtPosition(vec2(30,30))
            local monsterTile = dungeon:getTile(vec2(34,34))
            _:expect(dungeon:manhattanToPlayer(monsterTile)).is(8)
            tiles = dungeon:availableTilesCloserToPlayer(monsterTile)
            x1,x2 = dungeon:getTile(vec2(33,34)), dungeon:getTile(vec2(34,33))
            _:expect(tiles).has(x1)
            _:expect(tiles).has(x2)
            tiles = dungeon:availableTilesFurtherFromPlayer(monsterTile)
            x1,x2 = dungeon:getTile(vec2(35,34)), dungeon:getTile(vec2(34,35))
            _:expect(tiles).has(x1)
            _:expect(tiles).has(x2)
            tiles = dungeon:availableTilesSameDistanceFromPlayer(monsterTile)
            x1,x2 = dungeon:getTile(vec2(33,35)), dungeon:getTile(vec2(35,33))
            _:expect(tiles).has(x1)
            _:expect(tiles).has(x2)
        end)

This doesn’t even check the empty case. Since we’ve refactored those methods to get at some inter-tile ones, we really need more tests for these.

I’ve made a note of that and I think we’ll start there tomorrow.

I also learned that it’s not as easy as it might be to set up a special purpose monster. Monsters are now created by the Monsters class. Creating one in GameRunner seems not to be quite the thing. We’ll work on that soon as well, perhaps also tomorrow.

For now … we made some decent progress on improving out Map pathfinding, and we have what seems to be a HangoutMonsterStrategy ready to go but untested.

A decent day, but not a great one. We’ll try to do better tomorrow.

Commit: HangoutMonsterStrategy and refactoring.

Rather a large commit, hit 6 files and 90 lines added or changed. That’s surely larger than I’d like to do. Should try to bear down on smaller more frequent commits.

See you next time!


D2.zip