Dungeon 240--Options
My commitment for the day is to be using the new Map objects for something in the actual game. I think I see at least a couple of ways to go. Spoiler: It’s in!
Yesterday I floated the idea of plugging in the new Map in addition to the existing code for managing tiles. Then we could incrementally move old operations into the new Map, until nothing remained to move, and then we could just remove the old self.tiles
array. In essence, that approach is the one that Michael Feathers called “strangler” in his excellent book, Working Effectively with Legacy Code. (I don’t know whether he originated the term or not. His book is where I first remember seeing it.)
The term refers to so-called “strangler” vines that can choke out other growth. For those who prefer a gentler term, I’m sorry, I don’t have one, but if you have suggestions, I’ll update this article with them. The term notwithstanding, it’s a good way to go. You can take your time and just peck away at removing the old code at whatever pace you can manage.
I had another idea this morning while feeding the Cat Goddess, which was that currently the Tile has a member variable position
, consisting of a vec2
containing the Tile’s x and y coordinate values. There’s nothing to stop us from preserving that behavior, and the method that accesses it, Tile:pos()
. If we did that, a lot of the Tile’s behavior might “just work”.
Either way, we need to deal with the unpleasant fact of supporting what a Tile needs. Its init is:
function Tile:init(x,y,kind, runner)
if runner ~= Runner then
print(runner, " should be ", Runner)
error("Invariant Failed: Tile must receive Runner")
end
self.position = vec2(x,y)
self.kind = kind
self.runner = runner
self:initDetails()
end
When the Dungeon is created, kind
is always TileEdge
, but Tiles do get different types. I’m not sure whether we replace them or just tell them to be of a different type. Not important now. The current Map is given a class and creates an instance with this code:
function Map:createRectangle(xWidth, zHeight, klass)
self.tab = {}
for z = 1,zHeight do
for x = 1,xWidth do
local pt = self.mapType(x,z)
local item = klass(pt)
self:atPointPut(pt,item)
end
end
end
That is, we pass in only the pt
, which is a MapPoint instance, configured as cartesian or hex.
Hm. I note that we haven’t told our MapPoint which kind of coordinate it is. In essence, it doesn’t care, its calculations work either way. We have to be sure to give only cartesian changes to a cartesian MapPoint, but MapPoint doesn’t think it’s his problem. We could change that, of course. At one point we had HexPoints and CartesianPoints. We collapsed those by removing duplication. Did we go too far? Perhaps. If so, we can fix it.
MapPoint has the ability to return its coordinates, so if we want to try preserving position
, we can do so readily. What about the need for runner
? I can see two ways to go. One would be to extend Map
to allow us to provide additional parameters to be sent to the created class. The other would be to rely on the fact that there is a global, Runner, and allow the Tile
to access it that way. I am inclined toward the former solution. It’s better design, in my opinion.
OK, we’ll do that. I’m not dead certain of the syntax for doing this, so I’m going to go back to D3 to put it in, and then do it in D2.
_:test("Additional Parameters", function()
local map = Map:cartesianMap(2,2, ParmTile, 666, 777)
local tile = map:atXYZ(1,0,1)
_:expect(tile.p1).is(666)
_:expect(tile.p2).is(777)
end)
Now I need ParmTile …
ParmTile = class()
function ParmTile:init(aPoint, p1, p2)
self.point = aPoint
self.p1 = p1
self.p2 = p2
end
Now the test should file not finding the values. That’s what happens. Now in Map:
function Map:createRectangle(xWidth, zHeight, klass)
self.tab = {}
for z = 1,zHeight do
for x = 1,xWidth do
local pt = self.mapType(x,z)
local item = klass(pt)
self:atPointPut(pt,item)
end
end
end
That becomes …
function Map:createRectangle(xWidth, zHeight, klass, ...)
self.tab = {}
for z = 1,zHeight do
for x = 1,xWidth do
local pt = self.mapType(x,z)
local item = klass(pt, ...)
self:atPointPut(pt,item)
end
end
end
The ...
does the job. However, we need to push the ...
all the way up to the various creation methods:
function Map:hexMap(xWidth, zHeight, klass, ...)
return Map(xWidth, zHeight, hex, klass, ...)
end
function Map:cartesianMap(xWidth, zHeight, klass, ...)
return Map(xWidth, zHeight, cartesian, klass, ...)
end
function Map:init(xWidth, zHeight, mapType, klass, ...)
assert(mapType == cartesian or mapType == hex, "MapType must be cartesian or hex")
self.mapType = mapType
self:createRectangle(xWidth, zHeight, klass, ...)
end
With that in place, the test runs. Commit D3: added additional parameters to object created in Map.
Now I think I’ll just paste that stuff back to D2, the changes were extensive enough to warrant that. Commit D2: added additional parameters to object created in Map.
Warning, Will Robinson …
We have a plumber here working on things, and it’s necessary for me to chat with him as he reports his discoveries and plans. That means that I’m more distracted than I’d like to be. I’ll try to proceed with caution, expecting trouble. Frequent readers may be saying “Really, he ought to do that more often …”
Now I think in that dcPrepare
method, we can create our additional map and then see what we can do with it.
Darn, I have a lot of tabs in this program.
Hey, wait:
function GameRunner:dcPrepare()
TileLock = false
DungeonContents = DungeonContentsCollection()
self:createNewDungeon()
local dungeon = self.dungeon
self.tiles = dungeon:createTiles(self.tileCountX, self.tileCountY)
dungeon:clearLevel()
Announcer:clearCache()
end
The DELETED GameRunner has the tiles. I suppose at some level I knew that, but I’ve been thinking about things in the Dungeon class. I’d best review what GameRunner does with the tiles.
Good news! All it does is loop over the tiles to do the draw. This may turn out to be less trouble than I feared.
Sticking with the plan, let’s add a map. No, I’ve got to review a bit more.
OK. What you just read is the sort of thing you rarely read in a book or article about how to program. Those things always seem to have this magical godlike ability to do the right thing the first time. That’s not my style. My style is to share with you what really happens, what I discover, what I forget. I imagine that you’re probably similar in that sometimes you don’t remember or surprise yourself. Probably less often than I do, but I think it’s important that we remember that we are human, and that we have to expect that we’ll make human errors.
If you are not human, feel free to ignore the above.
Dungeon has the tiles, and I’m back on the track of having Dungeon have a Map as well. We’ll deal with the draw in GameRunner by forwarding it to Dungeon, which probably should have already happened.
First Connection
Here’s where we’re working:
function Dungeon:createTiles(tileCountX, tileCountY)
self.tileCountX = tileCountX
self.tileCountY = tileCountY
self.tiles = {}
for x = 1,tileCountX+1 do
self.tiles[x] = {}
for y = 1,tileCountY+1 do
local tile = Tile:edge(x,y, self.runner)
self:defineTile(tile)
end
end
return self.tiles
end
We “just” have to create the map here as well. Our Map, even with the ability to pass parameters, isn’t quite smart enough to call a creation method like edge
, but we can look to see what that does:
function Tile:edge(x,y, runner)
return Tile(x,y,TileEdge, runner)
end
Unfortunately, TileEdge
is local to Tile. I’ll just let it be global for now.
function Dungeon:createTiles(tileCountX, tileCountY)
self.tileCountX = tileCountX
self.tileCountY = tileCountY
self.tiles = {}
for x = 1,tileCountX+1 do
self.tiles[x] = {}
for y = 1,tileCountY+1 do
local tile = Tile:edge(x,y, self.runner)
self:defineTile(tile)
end
end
self.map = Map(tileCountX, tileCountY, Tile, TileEdge, self.runner)
return self.tiles
end
function Dungeon:createTiles(tileCountX, tileCountY)
self.tileCountX = tileCountX
self.tileCountY = tileCountY
self.tiles = {}
for x = 1,tileCountX+1 do
self.tiles[x] = {}
for y = 1,tileCountY+1 do
local tile = Tile:edge(x,y, self.runner)
self:defineTile(tile)
end
end
self.map = Map:cartesianMap(tileCountX, tileCountY, Tile, TileEdge, self.runner)
return self.tiles
end
This “should” work, but of course it doesn’t, because Tile isn’t prepared to be created by a call from Map. There’s the rub.
I’m not sure what to do here. Converting Tile all at once to live in a Map seems daunting. But maybe it really isn’t. We are pretty sure that except for the draw, only the Dungeon messes with the tiles array, so maybe we can just ditch it and convert Tile more or less blindly. Let’s try that.
Revert, regroup.
function GameRunner:drawMap(tiny)
fill(0)
stroke(255)
strokeWidth(1)
for i,row in ipairs(self.tiles) do
for j,tile in ipairs(row) do
tile:draw(tiny)
end
end
end
We’ll forward this to Dungeon to do the work:
function Dungeon:drawMap(tiny)
for i,row in ipairs(self.tiles) do
for j,tile in ipairs(row) do
tile:draw(tiny)
end
end
end
Should still work. Does. Commit: defer drawMap from GameRunner to Dungeon.
This kind of thing is often made up of tiny steps like this, and where possible, I find it best to commit after each one, because once in a while, a step doesn’t work and I need to back out. The fact that I find it best does not imply that I always remember to do it.
Now I think we can tell GameRunner not to save the tiles:
Before:
function GameRunner:dcPrepare()
TileLock = false
DungeonContents = DungeonContentsCollection()
self:createNewDungeon()
local dungeon = self.dungeon
self.tiles = dungeon:createTiles(self.tileCountX, self.tileCountY)
dungeon:clearLevel()
Announcer:clearCache()
end
After:
function GameRunner:dcPrepare()
TileLock = false
DungeonContents = DungeonContentsCollection()
self:createNewDungeon()
local dungeon = self.dungeon
dungeon:createTiles(self.tileCountX, self.tileCountY)
dungeon:clearLevel()
Announcer:clearCache()
end
Test that a bit. Seems good. Commit: GameRunner no longer knows tiles.
Whee. We still have the issue that Tile isn’t prepared to receive a coordinate when it’s created.
I think we can go about this in tiny steps as well. We create Tiles like this:
function Tile:hall(x,y, runner)
assert(not TileLock, "Attempt to create room tile when locked")
return Tile(x,y,TileHall, runner)
end
function Tile:room(x,y, runner)
assert(not TileLock, "Attempt to create room tile when locked")
return Tile(x,y,TileRoom, runner)
end
function Tile:wall(x,y, runner)
assert(not TileLock, "Attempt to create wall tile when locked")
return Tile(x,y,TileWall, runner)
end
function Tile:edge(x,y, runner)
return Tile(x,y,TileEdge, runner)
end
function Tile:init(x,y,kind, runner)
if runner ~= Runner then
print(runner, " should be ", Runner)
error("Invariant Failed: Tile must receive Runner")
end
self.position = vec2(x,y)
self.kind = kind
self.runner = runner
self:initDetails()
end
Let’s change all the creation methods to accept a single parameter, coordinates, instead of two x and y. No … well, maybe. We do need to change the init, and to change the meaning of .position
to be a MapPoint. We could, however, leave the creation methods’ parameter lists alone. Let’s review how they are used.
There are about a million tests using Tile:hall
, and two live uses. Two uses of :edge
. No uses of :wall
, but one use of TileWall
to change the kind of a tile. One use of :room
.
There are two different ways to change a Tile from the initial form, Edge, to another form, Room, Wall, or Hall. Sometimes the code replaces the tile in the tiles
arrays. Sometimes it just tells the Tile to change its kind
. The former form is arguably better, because it allows the Tile to be immutable, and that’s usually a good thing. The former form has the advantage that it doesn’t change the contents of the tiles array.
We should pick one and live with it.
The most irritating thing is that there are innumerable tests that create tiles and do things with them. These will all need changing.
We Need a Plan Here
OK, we need to decide what to do. The basic idea, still, is that we’ll have both the tiles
array and a Map
in Dungeon
, for a while. To allow that to work, we need to make Tile compatible between the two uses.
What if we made a simple TileMapAdapter class, compatible with Map expectations, with the ability to … uh oh … with the ability to create Tiles in the old scheme.
I said “uh oh” because I just realized that while I can certainly index the tiles either through a Map or the old tiles array, I need the same tiles in each structure, because there are contents and such to be dealt with.
We’ve still not found the tiny change that brings us to our desired point, some part of the app running on the Map.
My first thought was, OK, build the tiles arrays and copy the tiles into the Map. But Map wants to create its own. We could just replace them all. Nasty but doable. Or we could create the Map and copy its tiles over to our array.
I think I’ll try the former, kind of invasive but I think we can do it.
Ah. First this:
function Dungeon:createTiles(tileCountX, tileCountY)
self.tileCountX = tileCountX
self.tileCountY = tileCountY
self.tiles = {}
for x = 1,tileCountX+1 do
self.tiles[x] = {}
for y = 1,tileCountY+1 do
local tile = Tile:edge(x,y, self.runner)
self:defineTile(tile)
end
end
self.map = Map:cartesianMap(tileCountX, tileCountY, FakeTile)
end
local FakeTile = class()
function FakeTile:init()
end
This creates a map, but populates it with instances of FakeTile, which has no content or behavior. Now I can “just” stuff the Tiles from the other array in there.
Map knows atPointPut
. We can use that:
function Dungeon:createTiles(tileCountX, tileCountY)
self.tileCountX = tileCountX
self.tileCountY = tileCountY
self.tiles = {}
for x = 1,tileCountX+1 do
self.tiles[x] = {}
for y = 1,tileCountY+1 do
local tile = Tile:edge(x,y, self.runner)
self:defineTile(tile)
end
end
self.map = Map:cartesianMap(tileCountX+1, tileCountY+1, FakeTile)
for x = 1,tileCountX+1 do
for y = 1,tileCountY+1 do
local t = self.tiles[x][y]
local pt = MapPoint:cartesian(x,y)
self.map:atPointPut(pt,t)
end
end
end
This is nasty, but contained. Game still runs. Commit: Map created matching Dungeon.tiles.
Maybe we can test the map using draw.
function Dungeon:drawMap(tiny)
for c,t in pairs(self.map.tab) do
t:draw(tiny)
end
--[[
for i,row in ipairs(self.tiles) do
for j,tile in ipairs(row) do
tile:draw(tiny)
end
end
--]]
end
Doesn’t quite work, because of that define tile thing:
function Dungeon:defineTile(aTile)
assert(not TileLock, "attempt to set tile while locked")
local pos = aTile:pos()
self.tiles[pos.x][pos.y] = aTile
end
Extend that:
function Dungeon:defineTile(aTile)
assert(not TileLock, "attempt to set tile while locked")
local pos = aTile:pos()
self.tiles[pos.x][pos.y] = aTile
local pt = MapPoint:cartesian(pos.x,pos.y)
self.map:atPointPut(pt,aTile)
end
Doesn’t work because we use it before we have the map. No problem, build the map first.
function Dungeon:createTiles(tileCountX, tileCountY)
self.tileCountX = tileCountX
self.tileCountY = tileCountY
self.map = Map:cartesianMap(tileCountX+1, tileCountY+1, FakeTile)
self.tiles = {}
for x = 1,tileCountX+1 do
self.tiles[x] = {}
for y = 1,tileCountY+1 do
local tile = Tile:edge(x,y, self.runner)
self:defineTile(tile)
end
end
for x = 1,tileCountX+1 do
for y = 1,tileCountY+1 do
local t = self.tiles[x][y]
local pt = MapPoint:cartesian(x,y)
self.map:atPointPut(pt,t)
end
end
end
This nearly works:
My guess on this is that the issue is that we’re not drawing in any particular order. Let’s check that with a bit of a hack.
After a bit of messing about, I did this:
function Dungeon:drawMap(tiny)
if OneTime then return end
for x = 1, self.tileCountX do
for y = 1, self.tileCountY do
local pt = MapPoint:cartesian(x,y)
local mapTile = self.map:atPoint(pt)
local oldTile = self.tiles[x][y]
assert(mapTile==oldTile, "mismatch "..tostring(mapTile).." "..tostring(oldTile))
mapTile:draw(tiny)
end
end
OneTime = true
end
I’m fetching what should be the same tile from each map, and asserting them identical. Some of them are not, returning an edge tile (which does not draw anything) for the map and a room tile for the tiles table. Let me try a trick:
Trick was this:
function Dungeon:drawMap(tiny)
if OneTime then return end
local old = 0
local map = 0
for x = 1, self.tileCountX do
for y = 1, self.tileCountY do
local pt = MapPoint:cartesian(x,y)
local mapTile = self.map:atPoint(pt)
local oldTile = self.tiles[x][y]
if oldTile == mapTile then
mapTile:draw(tiny)
map = map + 1
else
oldTile:draw(tiny)
old = old + 1
end
end
end
print("old", old, "map", map)
OneTime = true
end
Still doesn’t draw. I need to revert again.
Then I move the map creation up again:
function Dungeon:createTiles(tileCountX, tileCountY)
self.tileCountX = tileCountX
self.tileCountY = tileCountY
self.map = Map:cartesianMap(tileCountX+1, tileCountY+1, FakeTile)
self.tiles = {}
for x = 1,tileCountX+1 do
self.tiles[x] = {}
for y = 1,tileCountY+1 do
local tile = Tile:edge(x,y, self.runner)
self:defineTile(tile)
end
end
for x = 1,tileCountX+1 do
for y = 1,tileCountY+1 do
local t = self.tiles[x][y]
local pt = MapPoint:cartesian(x,y)
self.map:atPointPut(pt,t)
end
end
end
Let’s check that define. It needs to be changed to define both. Lost that in a revert:
function Dungeon:defineTile(aTile)
assert(not TileLock, "attempt to set tile while locked")
local pos = aTile:pos()
self.tiles[pos.x][pos.y] = aTile
local pt = MapPoint:cartesian(pos.x,pos.y)
self.map:atPointPut(pt,aTile)
end
Test. Works. Commit: force map to have same tiles as Dungeon.tiles.
Hey! Where’s my pair? Given that code in defineTile
, we surely don’t need to copy the map over.
Before:
function Dungeon:createTiles(tileCountX, tileCountY)
self.tileCountX = tileCountX
self.tileCountY = tileCountY
self.map = Map:cartesianMap(tileCountX+1, tileCountY+1, FakeTile)
self.tiles = {}
for x = 1,tileCountX+1 do
self.tiles[x] = {}
for y = 1,tileCountY+1 do
local tile = Tile:edge(x,y, self.runner)
self:defineTile(tile)
end
end
for x = 1,tileCountX+1 do
for y = 1,tileCountY+1 do
local t = self.tiles[x][y]
local pt = MapPoint:cartesian(x,y)
self.map:atPointPut(pt,t)
end
end
end
After:
function Dungeon:createTiles(tileCountX, tileCountY)
self.tileCountX = tileCountX
self.tileCountY = tileCountY
self.map = Map:cartesianMap(tileCountX+1, tileCountY+1, FakeTile)
self.tiles = {}
for x = 1,tileCountX+1 do
self.tiles[x] = {}
for y = 1,tileCountY+1 do
local tile = Tile:edge(x,y, self.runner)
self:defineTile(tile)
end
end
end
Still works of course. Commit: remove copy of tiles to map from Dungeon, redundant.
Let’s pop that assert back in the draw and make sure we’ve got all the same tiles still.
function Dungeon:drawMap(tiny)
for i,row in ipairs(self.tiles) do
for j,tile in ipairs(row) do
local pos = tile:pos()
local pt = MapPoint:cartesian(pos.x, pos.y)
local mapTile = self.map:atPoint(pt)
assert(mapTile==tile, "mismatch "..tostring(tile).." "..tostring(mapTile))
tile:draw(tiny)
end
end
end
Assert doesn’t trigger. Surely now I can draw the other tile:
Yes! Commit: Dungeon:drawMap uses map tiles, loops in array order.
Now I suspect that if we change it to draw in the map’s natural order, we’ll get that spotty display again.
function Dungeon:drawMap(tiny)
for pos,tile in pairs(self.map.tab) do
if tile:is_a(Tile) then
tile:draw(tiny)
end
end
end
I found that I’m not always getting a tile back. I’m not sure what is coming back, it seems to be a table of some kind. When I leave that is_a
in, the game plays correctly.
Let’s commit: Dungeon.map used to draw. Hacked to check for Tile.
Now let’s see what else might come back. How can there be anything in the pairs other than Tiles? Oh … there could be a FakeTile. Let’s see if that’s what it is.
I had to rename FakeTile to DummyTile because there was already a FakeTile class and the Dungeon code wasn’t linking up correctly. When I do that, I find that there are lots of fake tiles. Let’s learn more:
function Dungeon:drawMap(tiny)
if OneTime then return end
local ct = 0
for pos,tile in pairs(self.map.tab) do
if tile:is_a(DummyTile) then
print("DummyTile at", pos)
ct = ct + 1
end
if tile:is_a(Tile) then
tile:draw(tiny)
end
end
print("ct", ct)
OneTime = true
end
That should give a clue, even to me. Wow, it prints 5590 and the picture doesn’t draw any more. That’s all the tiles. Revert.
Drawing is OK. Code is:
function Dungeon:drawMap(tiny)
for pos,tile in pairs(self.map.tab) do
if tile:is_a(Tile) then
tile:draw(tiny)
end
end
end
Now I think we know that what’s in there is an old-style FakeTile, not the new one. If I just rename the local class to DummyTile, does it work?
Yes. Commit: move and rename DummyTile.
I noticed something odd, when I was printing the DummyTiles, which was that the print seemed to come out incrementally. I think that part of what’s going on is that the dungeon is created and laid out while draw is running. I think we have a timing problem. I think that once the map is correct, everything may be OK.
But I’m not entirely sure of that, because I’m not sure whether draw can run at all when other code is running.
Let’s create a special button to check the map for having only real tiles.
parameter.action("CheckMap", function()
if Runner.dungeon.map then
local found = 0
for pos,tile in pairs(Runner.dungeon.map.tab) do
if not tile:is_a(Tile) then
print(pos,tile)
found = found+1
end
end
print(found, " found")
end
end)
That does not work as anticipated. If finds DummyTile in every location, even while the game is running fine.
The draw is this:
function Dungeon:drawMap(tiny)
for pos,tile in pairs(self.map.tab) do
if tile:is_a(Tile) then
tile:draw(tiny)
end
end
end
I don’t understand how these two facts are compatible. There should be no stable point at which there is a map containing only DummyTile. And if there is such a map, how could the drawing code be working?
I make this change:
function Dungeon:createTiles(tileCountX, tileCountY)
self.tileCountX = tileCountX
self.tileCountY = tileCountY
self.map = Map:cartesianMap(1,1, DummyTile)
self.tiles = {}
for x = 1,tileCountX+1 do
self.tiles[x] = {}
for y = 1,tileCountY+1 do
local tile = Tile:edge(x,y, self.runner)
self:defineTile(tile)
end
end
end
That now creates a Map with just one element, a DummyTile. Since the Map doesn’t check its indices, when we call defineTile
, things just get added in. Now when I press the CheckMap button, it finds just one DummyTile, the one at 1,0,1.
Where are the other ones? In particular, why isn’t the one at 1,0,1 replaced?
Oh this is marvelous, squared. First of all, I left a duplicate method defineTile
when I added in my print information. But second, the values of pos.x and pos.y are floats, not integers, because vectors have floating coordinates. So the keys for the new tiles are not the same as the old ones. So it works, but there will be DummyTiles in there.
We need to fix this at the very bottom, I think.
Add a test:
_:test("MapPoint Creation", function()
local coord = MapPoint:hex(0,0,0)
_:expect(coord:invalid()).is(false)
local f = function() MapPoint:hex(1,1,1) end
_:expect(f).throws("Invalid HexCoordinates 1,1,1")
f = function() MapPoint(1,0,-1) end
_:expect(f).throws("Invalid call to MapPoint(1,0,-1)")
coord = MapPoint:hex(1.0, 0, -1.0)
_:expect(coord:key()).is("1,0,-1")
end)
Fails as expected:
1: MapPoint Creation -- Actual: 1.0,0,-1.0, Expected: 1,0,-1
Fix:
function MapPoint:init(x,y,z, secret)
self._x = x//1
self._y = y//1
self._z = z//1
self.keyString = x..","..y..","..z
assert(secret, "Invalid call to MapPoint("..self.keyString..")")
end
Curiously, that still fails. I guess //1 forces to integer value but not to integer? We can try math.floor …
This, too, still thinks the thing is a float. This is more than a little irritating. I can see a couple of things to do. One would be to force the inputs to float and accept floating keys everywhere. They’d be longer but otherwise presumably OK. Another would be to use string.format to create my keys. I like that better, because I honestly don’t want any fractions showing up here.
function MapPoint:init(x,y,z, secret)
self._x = x//1
self._y = y//1
self._z = z//1
self.keyString = string.format("%d,%d,%d", self._x, self._y, self._z)
assert(secret, "Invalid call to MapPoint("..self.keyString..")")
end
That makes my test pass, and should make sure that all the Map keys look like “3,-8,5”, not “3.0,-8.0,5.0”. Now I expect the CheckMap not to find anything.
And it does not. Now we should be able to change this not to check for the tile type:
function Dungeon:drawMap(tiny)
for pos,tile in pairs(self.map.tab) do
tile:draw(tiny)
end
end
And it works. And we are there.
Commit: Game uses new Map object to draw. Map is created to duplicate existing dungeon.tiles for now.
Whew
We’ve accomplished the day’s goal, which was to have the game running and using the Map for something.
Let’s reflect on what happened, and what we’ve learned.
- Need Identical Tiles
- Even with a more robust way of creating the Map, we couldn’t readily create both the
self.tiles
andself.map
maps by calling the creation method, because we need the identical tiles in both maps. So until we’re doing everything with the Map, we have a kludge going on where we copy theself.tiles
tiles into theself.map
. - Floating Point Pos
- The most interesting issue was that the old Tiles’
pos
variable is a vector, and when I usedpos
to define the Tile into both map and tiles collection, I wound up with floating point numbers, which Lua duly converted to a key of “1.0” where I wanted “1”. That was probably at the center of most of my confusion about why things wouldn’t draw and why there were DummyTile instances in the Map. - Demeter Violation
- We are ripping the table
tab
out of Map to draw. We should keep that inside Map, I want a clear head to decide how best to do that. Either we need a callback, which we certainly can have, or we need to have apairs
function on Map. I’m not sure which I should prefer.
There were quite a few missteps today, more than I can explain away by plumber interruptions. ($320, by the way). I think most of them came from the floating point indexes in the Map. May need to think about that. I also wonder if there are floating point indexes in the other tiles table, or if Lua has a trick for that. It must have, or things wouldn’t work at all.
I stumbled a bit, worried that GameRunner knew too much about tiles, but that was readily fixed by deferring its draw on down to Dungeon. It did leave me wrong-footed for a while, though.
There were, and still are, two ways of changing what kind of Tile is in the map. Usually, we create a new Tile of the desired kind, and plonk it in. In at least one case, the code simply changes the kind
of the Tile directly. We should stick to one way of doing that.
However
However, my overall feeling is that this has gone well enough, though a bit raggedly. I think the remaining changes are likely to be straightforward, although there are quite a few of them. I expect that we’ll come up with a few central methods on Dungeon and/or Map to sort through these.
It took a bit longer than I’d like, but the Map is in the game and being used.
Once we get it all the way in, we’ll see about creating a hexagonal map of some kind. Current guess is that we’re doing better than I feared. We’ll see. Something will surely go wrong.
See you next time!