Dungeon 249
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!