Let’s continue to prepare the Dungeon class to work on hexes as well as square tiles. I expect no real trouble. I look forward to learning why my expectations will not be met.

I’ve been thinking about our notion of “distance”. Generally we have methods about “closer” and “further”, but we have some that mention just “distance”, and then, drilling down, some that mention “manhattanDistance”. We also have at least one method that is using vector distance, which isn’t the same as manhattan at all.

I think we should stick to manhattan at the game level, if we possibly can, but I am OK calling it distance, because it is the distance that the princess would have to walk to get somewhere. (Monsters can move diagonally sometimes. Clever, these monsters.) So I might take one more look at where the “distance” calls come down to “manhattanDistance”. That distinction is one between game, and geometry, so if it’s in about the right place, it should be OK.

And I want those vector distances right out, but we’ll have to take a careful look at them, because decisions made on geometric distance will be made differently on manhattan distance. With any luck, we won’t care. Let’s do that first.

function Tile:distance(aTile)
    return self:pos():dist(aTile:pos())
end

function Tile:illuminateLine(dx,dy)
    local max = IlluminationLimit
    local pts = Bresenham:drawLine(0,0,dx,dy)
    for i,offset in ipairs(pts) do
        local pos = self:pos() + offset
        local d = self:pos():dist(pos)
        if d > max then break end
        local tile = self.runner:getTile(pos)
        tile:setVisible(d)
        if tile.kind == TileWall then break end
    end
end

The second one is doing geometry, and I think we’ll allow it. Conveniently, it isn’t going through the distance method. Does anyone use that?

I find these:

function Entity:distance(aTile)
    return aTile:distance(self:getTile())
end

function Dungeon:furthestFrom(target, r1, r2)
    local d1 = r1:distance(target)
    local d2 = r2:distance(target)
    if d2 > d1 then return r2 else return r1 end
end

What even are r1 and r2 here?

function Dungeon:farawayOpenRoomTile(room, target)
    local candidate
    local result = self:randomRoomTile(room)
    for i = 1,20 do
        candidate = self:randomRoomTile(room)
        result = self:furthestFrom(target, result, candidate)
    end
    return result
end

This method is used to find places, well, far away from other places. Manhattan distance will be just fine for this.

I can see no references to Entity:distance. I’m also finding that “manhattan” is used fairly high up in the hierarchy. I’ll probably change my mind and allow that, but one way or the other, I’d prefer either just one term throughout or one at game level and one down in the geometry.

Let’s see about removing Entity:distance. All goes well. We’ll commit: removed unused Entity:distance method.

A Nice Codea Feature

While Codea isn’t anywhere near as powerful as a real IDE, it does have some nice features. One that I’m particularly fond of is that if you select a method name, one of the popup menu items is “Find” and touching that will show you all the lines containing that word. If you want a bit more precision, it’s easy enough to add a “:” on the front and “(“ on the back.

It’s not much, but it makes untangling senders of methods a lot easier. We are grateful for small things.

The only sender of Tile:distance seems to be this one:

function Dungeon:furthestFrom(target, r1, r2)
    local d1 = r1:distance(target)
    local d2 = r2:distance(target)
    if d2 > d1 then return r2 else return r1 end
end

First, please, let’s rename those parameters to something connoting what they are, Tiles.

function Dungeon:furthestFrom(targetTile, tile1, tile2)
    local d1 = tile1:distance(targetTile)
    local d2 =tile2:distance(targetTile)
    if d2 > d1 then return tile2 else return tile1 end
end

Yesterday, we started our multi-dispatch far better quite fine way of getting the distance between tiles with this method:

function Tile:distanceFrom(aTile)
    return self.mapPoint:distanceFromTile(aTile)
end

We’ll apply this here and remove the old Tile:distance. Should have no discernible impact.

function Dungeon:furthestFrom(targetTile, tile1, tile2)
    local d1 = tile1:distanceFrom(targetTile)
    local d2 =tile2:distanceFrom(targetTile)
    if d2 > d1 then return tile2 else return tile1 end
end

Test. We’re all good. I ran into a couple of mimics and had a tough battle, but I had stocked up on health potions, which saved me. Commit: remove old Tile:distance method, using new manhattan distance.

Now let’s see who’s using Tile:pos(), which is returning that vec2 that we’d like to be rid of.

function Dungeon:defineTile(aTile)
    assert(not TileLock, "attempt to set tile while locked")
    local pos = aTile:pos()
    local pt = Maps:point(pos.x,pos.y)
    self.map:atPointPut(pt,aTile)
end

This one is particularly strange, since the Tile actually has a MapPoint that we could just fetch. However, I wonder whether we need this at all. Generally once a Tile is created it never changes. If I recall, however, there was an exception. Find.

function GameRunner:defineTile(aTile)
    self:getDungeon():defineTile(aTile)
end

function Dungeon:setHallAndDoors(prevTile,currTile, dir)
    local pos = currTile:pos()
    if not currTile:isRoom() then
        currTile = Tile:hall(pos.x,pos.y,currTile.runner)
        self:defineTile(currTile)
        if prevTile:isRoom() then
            currTile:setCanBeDoor(dir)
        end
    else -- curr is room, may need to set prev
        if prevTile:isHall() then
            prevTile:setCanBeDoor(dir)
        end
    end
    return currTile
end

function Dungeon:setHallwayTile(x,y)
    local t = self:privateGetTileXY(x,y)
    if not t:isRoom() then
        self:defineTile(Tile:hall(x,y,t.runner))
    end
end

function Room:paint()
    for x = self.x1,self.x2 do
        for y = self.y1,self.y2 do
            self.runner:defineTile(self:correctTile(x,y))
        end
    end
end

Ah, I see what we’re up to here, we’re trying to let the Tiles be immutable. That’s probably a good idea. So let’s just convert the top guy to do it right.

We do not have an accessor for the Tile’s mapPoint. We do have one object fetching it directly (bad Ron no biscuit) but no one else. For defineTile we’d prefer not to have to rip its guts out again. What can we do?

function Dungeon:defineTile(aTile)
    assert(not TileLock, "attempt to set tile while locked")
    local pos = aTile:pos()
    local pt = Maps:point(pos.x,pos.y)
    self.map:atPointPut(pt,aTile)
end

We can tell the Tile to do something. Like this:

function Dungeon:defineTile(aTile)
    assert(not TileLock, "attempt to set tile while locked")
    aTile:defineInMap(self.map)
end

That’s better, but I bet we change it, or add to it, before we’re done here.

function Tile:defineInMap(aMap)
    aMap:atPointPut(self.mapPoint, self)
end

I expect this to work. Test. Things are good. Commit: Dungeon:defineTile defers to Tile:defineInMap.

OK, good so far but what about those people who call that method? They’re a bit naff as well.

function Dungeon:setHallAndDoors(prevTile,currTile, dir)
    local pos = currTile:pos()
    if not currTile:isRoom() then
        currTile = Tile:hall(pos.x,pos.y,currTile.runner)
        self:defineTile(currTile)
        if prevTile:isRoom() then
            currTile:setCanBeDoor(dir)
        end
    else -- curr is room, may need to set prev
        if prevTile:isHall() then
            prevTile:setCanBeDoor(dir)
        end
    end
    return currTile
end

OK let’s see what we are doing here. If the tile is a room tile, we create a hall tile at the same position (and store it into the currTile parameter, which really that stuff is really tacky). Then we put the new hall tile back in the same spot as the former room tile.

What does the Tile:hall method do?

function Tile:hall(x,y, runner)
    assert(not TileLock, "Attempt to create room tile when locked")
    return Tile(Maps:point(x,y),TileHall, runner)
end

function Tile:init(mapPoint,kind, runner)
    assert(mapPoint:is_a(MapPoint), "Tile requires MapPoint")
    if runner ~= Runner then
        print(runner, " should be ", Runner)
        error("Invariant Failed: Tile must receive Runner")
    end
    self.mapPoint = mapPoint
    self.kind = kind
    self.runner = runner
    self:initDetails()
end

Basically, it just sets kind to the TileHall value. All this to avoid modifying the Tile. And right below there we do modify the Tile:

        if prevTile:isHall() then
            prevTile:setCanBeDoor(dir)
        end

Meh. We’ll change the existing one:

function Dungeon:setHallAndDoors(prevTile,currTile, dir)
    local pos = currTile:pos()
    if not currTile:isRoom() then
        currTile:setToHall()
        if prevTile:isRoom() then
            currTile:setCanBeDoor(dir)
        end
    else -- curr is room, may need to set prev
        if prevTile:isHall() then
            prevTile:setCanBeDoor(dir)
        end
    end
    return currTile
end

Now we need that method. We have a method convertToEdge, so let’s name this one similarly.

function Dungeon:setHallAndDoors(prevTile,currTile, dir)
    local pos = currTile:pos()
    if not currTile:isRoom() then
        currTile:convertToHall()
        if prevTile:isRoom() then
            currTile:setCanBeDoor(dir)
        end
    else -- curr is room, may need to set prev
        if prevTile:isHall() then
            prevTile:setCanBeDoor(dir)
        end
    end
    return currTile
end
function Tile:convertToEdge()
    self.kind = TileEdge
end

But the more I look at this method, the more I realize that it has nothing to do with Dungeon and everything to do with Tiles. Feature envy again.

What this method is doing is deciding where to mark a possible door. We mark a door on a tile if the tile is a hallway tile and it is adjacent to a room tile. Because we carve hallways from one end to the other, this can occur either when we carve out of a room or into a room. That’s why the code above checks both prev and curr the way it does.

Let’s revert and look at this again.

function Dungeon:setHallAndDoors(prevTile,currTile, dir)
    local pos = currTile:pos()
    if not currTile:isRoom() then
        currTile = Tile:hall(pos.x,pos.y,currTile.runner)
        self:defineTile(currTile)
        if prevTile:isRoom() then
            currTile:setCanBeDoor(dir)
        end
    else -- curr is room, may need to set prev
        if prevTile:isHall() then
            prevTile:setCanBeDoor(dir)
        end
    end
    return currTile
end

The dir value tells us which way to draw the door picture.

This is tricky code. I wonder why I didn’t use elseif. What was I trying to accommodate or say? Maybe we should stay on mission and pick away at this bit by bit. Back to plan A, remove that one define:

function Dungeon:setHallAndDoors(prevTile,currTile, dir)
    local pos = currTile:pos()
    if not currTile:isRoom() then
        currTile:convertToHall()
        if prevTile:isRoom() then
            currTile:setCanBeDoor(dir)
        end
    else -- curr is room, may need to set prev
        if prevTile:isHall() then
            prevTile:setCanBeDoor(dir)
        end
    end
    return currTile
end

function Tile:convertToHall()
    self.kind = TileHall
end

Now I don’t need pos any more. Now there’s this:

function Dungeon:setHallwayTile(x,y)
    local t = self:privateGetTileXY(x,y)
    if not t:isRoom() then
        self:defineTile(Tile:hall(x,y,t.runner))
    end
end

This can use convertToHall:

function Dungeon:setHallwayTile(x,y)
    local t = self:privateGetTileXY(x,y)
    if not t:isRoom() then
        t:convertToHall()
    end
end

Commit: another use of convertToHall.

That use of privateGetTileXY is a clue that we are doing something weird, but it’s in the hallway carving, and I don’t want to go there yet.

Who else uses defineTile:?

function Room:paint()
    for x = self.x1,self.x2 do
        for y = self.y1,self.y2 do
            self.runner:defineTile(self:correctTile(x,y))
        end
    end
end

This, like the hallway stuff, is pretty cartesian in nature. I think we can leave it alone for present purposes. Back to people sending pos().

function Dungeon:neighbors(tile)
    local tPos = tile:pos()
    local offsets = self:neighborOffsets()
    return map(offsets, function(offset) return self:getTile(offset + tPos) end)
end

Didn’t we give the Map the ability to return the neighbors?

function BaseMap:surroundingObjects(aCoordinate)
    assert(aCoordinate, "nil coordinate")
    local points = Maps:surroundingPoints(aCoordinate)
    return self:pointsToObjects(points)
end

We should ask the Tile for its neighbors. As I dig into that, however, I realize that it’s a bit more tangled that I feel up to dealing with right now. The wise thing to do will be to stop while we’re ahead rather than dig into what is a bit messier than we’re up to.

And, oddly enough, I’m going to do the wise thing, for once. Let’s sum up.

Summary

We’ve done three useful commits this morning, discovering and removing a method that we had made obsolete, replaced an old distance with new manhattan distance, and changed defineTile to defer the work down to the Map where it belongs.

The smartest thing I did today was to stop before undertaking something I don’t feel up to. There’s no value to pushing through, it just results in fragile code. Better to wait, or do simple easy things. Like errands.

We’re pretty close to where we need to be to run on the Hex map. It’s probably time to build a simple Hex map and see what happens.

It’s worth noting that my estimate of the best possible time to get the Hex map working in the game was the end of last week. I think I said maybe it could take two weeks. I’m not ready to raise my hand and say it’ll take longer, but in order to know, I think we need to try it.

So, tomorrow, let’s try again to make a minimal playable hex map. It could happen.

See you then!