Reducing the span of concern for Tiles. Seem like a good idea. Will it be too difficult? Let’s find out. (Spoiler: Almost, but not too difficult.)

Yesterday we wrote two cards to remind us of something that seemed to be needed:

Issue 31: Runner is called during dungeon build, so we can’t just return the dungeon when done. Change Room and Tile to have Dungeon, not Runner.

Issue 32: Tile references its runner member variable mostly/only to get a dungeon. Change to have only Dungeon. See Issue 31.


Now the Real Reason that I want to change Tile to refer only to the dungeon, rather than the runner, is that during dungeon building, the Rooms paint themselves, and that means that the Tiles set themselves up, and the Tiles have a deplorable tendency to ask the Runner to do things for them. The methods called need to know the current Dungeon, and that means that the GameRunner needs to know the Dungeon instance before it’s built. That reference is strange, and it creates a temporal coupling in the code: certain things have to be done in a certain order. This is always true, of course, but in this case it’s certain things far separated from each other. Net net net, I don’t like it.

There is another reason why changing Tile not to know the GameRunner is a good idea. We certainly want the GameRunner to depend on the Dungeon to do things for it. And it makes sense that the Dungeon would depend on Tiles to do things for it. But then … if the Tile depends on the GameRunner to do things for it, we open up a big box of potential trouble. Here’s a nice low-level object, an innocent little tile, but it has access, in principle, to anything in the GameRunner … and via the GameRunner, in principle, to anything in the whole Game.

Managed carefully, that could be just fine. After all, we already have a Tile that turns out the lights, and we might want a tile that spawns new monsters or that rearranges the walls. We might want anything, and probably for some values of “anything”, that implies the GameRunner doing things at the behest of a Tile. Well, we’ll follow that path into the woods when we get there. It’s just not great design to have all kinds of references up and down the layers of code. Accidents could happen.

Whatever the reasons, We have a card that says we want to change Tile so that it knows the Dungeon, and not the Runner. Let’s do it.

Converting Tile

Tiles are created like this:

function Tile:init(mapPoint,kind, runner)
    assert(mapPoint:is_a(MapPoint), "Tile requires MapPoint")
    self.mapPoint = mapPoint
    self.kind = kind
    self.runner = runner
    self:initDetails()
end

They have a member variable runner, so if we use Codea’s Find to highlight uses of self.runner in the class, we can find all of them. We’ll begin with that review before we decide just how to proceed.

I began by carefully reviewing references to self.runner in Dungeon, not Tile. Ahem. Now that I’m looking at the right class …

function Tile:getNeighbor(aVector)
    local newPos = self:pos() + aVector
    return self.runner:getTile(newPos)
end

That does this:

function GameRunner:getTile(aPosition)
    return self:getDungeon():getTile(aPosition)
end

So this will cause us no trouble. There are a few calls to getTile.

function Tile:nearestContentsQueries()
    self.runner:getDungeon():nearestContentsQueries(self)
end

So this should be easy. We did the tricky one yesterday, when we changed the code that checked a tile to see if it had the player to check whether it contained a Player instance.

Let’s proceed like this. First, in init, where we are given a runner, we’ll just fetch the Dungeon from it, and save that, using it as needed. That should work without changing any other class. Then, we’ll look to the creators and fix them to pass the Dungeon instance directly.

function Tile:init(mapPoint,kind, runner)
    assert(mapPoint:is_a(MapPoint), "Tile requires MapPoint")
    self.mapPoint = mapPoint
    self.kind = kind
    self.dungeon = runner:getDungeon()
    self:initDetails()
end

Now …

function Tile:getNeighbor(aVector)
    local newPos = self:pos() + aVector
    return self.dungeon:getTile(newPos)
end

function Tile:getSurroundingInfo()
    local byte = 0
    local ck = { vec2(1,1),vec2(0,1),vec2(-1,1), vec2(1,0),vec2(-1,0), vec2(1,-1),vec2(0,-1),vec2(-1,-1) }
    for i,p in ipairs(ck) do
        byte = byte<<1
        local pos = self:pos() + p
        local tile = self.dungeon:getTile(pos)
        if tile:isFloor() then
            byte = byte|1
        end
    end
    return byte
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.dungeon:getTile(pos)
        tile:setVisible(d)
        if tile.kind == TileWall then break end
    end
end

function Tile:nearestContentsQueries()
    self.dungeon:nearestContentsQueries(self)
end

I expect this to work. Test. Tests do not run and I get a crash:

10: tile contents didn't leave -- Actual: table: 0x281a188c0, Expected: Player (15,15)
10: tile contents didn't arrive -- Actual: table: 0x281a14640, Expected: Player (15,15)
Tile:297: attempt to index a nil value (field 'dungeon')
stack traceback:
	Tile:297: in method 'getSurroundingInfo'
	Tile:157: in method 'convertEdgeToWall'
	Dungeon:194: in method 'convertEdgesToWalls'
	DungeonBuilder:154: in method 'defineDungeonLayout'
	DungeonBuilder:61: in method 'buildTestLevel'
	GameRunner:64: in method 'createTestLevel'
	Dungeon:21: in field '_before'
	CodeaUnit:44: in method 'test'
	Dungeon:33: in local 'allTests'
	CodeaUnit:16: in method 'describe'
	Dungeon:13: in function 'testDungeon'
	[string "testDungeon()"]:1: in main chunk
	CodeaUnit:139: in method 'execute'
	Main:13: in function 'setup'

Let’s check that test first, it might be accessing something mistakenly.

        _:before(function()
            _TileLock = TileLock
            _tweenDelay = tween.delay
            tween.delay = fakeTweenDelay
            _bus = Bus
            Bus = EventBus()
            runner = GameRunner(20,20)
            runner:defineDungeonBuilder()
            runner:createTestLevel(0)
            _dc = DungeonContents
            DungeonContents = DungeonContentsCollection()
            TileLock = false
        end)
        
        _:test("tile contents", function()
            local room = Room(10,10,11,11,runner)
            local tile = runner:getTile(vec2(15,15))
            local player = Player(tile,runner)
            _:expect(tile:getContents(), "started wrong").has(player)
            player:moveBy(vec2(1,0))
            _:expect(tile:getContents(), "didn't leave").hasnt(player)
            local t2 = runner:getTile(vec2(16,15))
            _:expect(t2:getContents(), "didn't arrive").has(player)
        end)

Yucch. This is one of those messy setups I was remarking on yesterday. A clear indication that things are too complex.

This is too tricky for me to untangle. I’m going to try a nasty trick and save the runner in Tile.

function Tile:init(mapPoint,kind, runner)
    assert(mapPoint:is_a(MapPoint), "Tile requires MapPoint")
    self.mapPoint = mapPoint
    self.kind = kind
    self.runner = runner -- <===
    self.dungeon = runner:getDungeon()
    self:initDetails()
end

If this runs, and I expect that it will, then … stop speculating, Ron, and run the tests. They fail, same way. Revert, use MMMSS1.

First save the dungeon and test:

function Tile:init(mapPoint,kind, runner)
    assert(mapPoint:is_a(MapPoint), "Tile requires MapPoint")
    self.mapPoint = mapPoint
    self.kind = kind
    self.runner = runner
    self.dungeon = runner:getDungeon()
    self:initDetails()
end
Spikes:126: attempt to call a nil value (method 'getInstanceIfPresent')
stack traceback:
	Spikes:126: in field 'callback'
	...in pairs(tweens) do
    c = c + 1
  end
  return c
end

:158: in upvalue 'finishTween'
	...in pairs(tweens) do
    c = c + 1
  end
  return c
end

:589: in function <...in pairs(tweens) do
    c = c + 1
  end
  return c
end

:582>

I think I lost yesterday’s change. Revert and test.

Right. I failed to commit that last change, I think. Fortunately I wrote it up.

function Tile:getPlayerIfPresent()
    local player = self.runner.player
    if self:contains(player) then return player else return nil end
end

We replaced that. Last time, I wrote a new method, like this:

function Spikes:toggleUpDown()
    local tile = self:getTile()
    if tile == nil then return end -- we're being called back from a previous dungeon.
    self.state = ({up="down", down="up"})[self.state]
    if self.stayDown then self.state = "down" end
    if self.state == "up" then
        local player = tile:getInstanceIfPresent(Player)
        if player then self:actionWith(player) end
    end
    self.delay(2, self.toggleUpDown, self)
end

function Tile:instanceOrNil(klass)
    for i,t in ipairs(self:getContents()) do
        if t:is_a(klass) then return t end
    end
    return nil    
end

Let’s instead just change Tile to do the same thing, so that Spikes don’t need to change:

function Tile:getPlayerIfPresent()
    for i,t in ipairs(self:getContents()) do
        if t:is_a(Player) then return t end
    end
    return nil
end

Test. Works, except that I find that the call to getInstance was present in Spikes, but had been lost from Tile. Somehow I got a partial commit or partial revert. Yikes, I hate it when it seems that the code manager did the wrong thing. Anyway tests are good. Commit: search for Tile containing player by class rather than instance.

Now, again, save dungeon but keep runner:

function Tile:init(mapPoint,kind, runner)
    assert(mapPoint:is_a(MapPoint), "Tile requires MapPoint")
    self.mapPoint = mapPoint
    self.kind = kind
    self.runner = runner
    self.dungeon = runner:getDungeon()
    self:initDetails()
end

This really better work. And it does. Commit: Tile saves a dungeon member variable. Still has runner.

Now change one reference:

function Tile:getNeighbor(aVector)
    local newPos = self:pos() + aVector
    return self.runner:getTile(newPos)
end

Just substituting dungeon for runner didn’t work. Let’s see what getTile really does, I must be confused.

function GameRunner:getTile(aPosition)
    return self:getDungeon():getTile(aPosition)
end

function GameRunner:getDungeon()
    return self.dungeon
end

How can that possibly fail? One impossible way would be if the Dungeon instance changed. Let me try this:

function Tile:getNeighbor(aVector)
    local newPos = self:pos() + aVector
    assert(self.dungeon==self.runner:getDungeon(), "dungeon has changed")
    return self.runner:getTile(newPos)
end

The assert triggers. Runner’s instance of dungeon has changed. It’s surely that darn temporal coupling.

This takes the wind out of my sails. I was in the mode of ticking through simple changes, and now I’m faced with a tangle. Different mindset. Let’s do a thing. Let’s refactor in Tile to use a getDungeon method, referring to the runner, so that when the time comes we can replace that single method.

function Tile:getDungeon()
    return self.runner:getDungeon()
end

Now I should be able to use that:

function Tile:init(mapPoint,kind, runner)
    assert(mapPoint:is_a(MapPoint), "Tile requires MapPoint")
    self.mapPoint = mapPoint
    self.kind = kind
    self.runner = runner
    self:initDetails()
end

function Tile:getNeighbor(aVector)
    local newPos = self:pos() + aVector
    return self:getDungeon():getTile(newPos)
end

Test. Works. Commit: New method Tile:getDungeon(), in partial use.

function Tile:getSurroundingInfo()
    local byte = 0
    local ck = { vec2(1,1),vec2(0,1),vec2(-1,1), vec2(1,0),vec2(-1,0), vec2(1,-1),vec2(0,-1),vec2(-1,-1) }
    for i,p in ipairs(ck) do
        byte = byte<<1
        local pos = self:pos() + p
        local tile = self:getDungeon():getTile(pos)
        if tile:isFloor() then
            byte = byte|1
        end
    end
    return byte
end

Test, commit: use new getDungeon() in Tile:getSurroundingInfo().

Clearly I could do them all, but I’m still in microstep mode. It’s a good place to be, though tiny commits are a slight pain on the iPad.

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:getDungeon():getTile(pos)
        tile:setVisible(d)
        if tile.kind == TileWall then break end
    end
end

Test, commit: use getDungeon in Tile:illuminateLine.

function Tile:nearestContentsQueries()
    self:getDungeon():nearestContentsQueries(self)
end

Test. Commit: Tile now refers to dungeon via getDungeon method throughout. getDungeon calls GameRunner:getDungeon for now. only one ref to runner.

OK. This is good. Tile is now ready to be converted to refer to a dungeon member variable as soon as it becomes stable in GameRunner.

Now let’s see if we can sort that out. But first, a break:

RESTrospective

We’re close to reducing the attention span of Tile, from Runner scope down to Dungeon scope, except that the GameRunner has this glitch where, apparently, it gets a new instance of Dungeon, surprising the heck out of Tile, if Tile wants to save it.

Somewhat unfortunately, Tiles have a fairly complex way of coming into being. Let’s review that, which we haven’t done lately.

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:room(x,y, runner)
    assert(not TileLock, "Attempt to create room tile when locked")
    return Tile(Maps:point(x,y),TileRoom, runner)
end

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

function Tile:edge(x,y, runner)
    return Tile(Maps:point(x,y),TileEdge, runner)
end

These are small factory methods and each is given a runner, so in principle we need to change those, although we can take our time, if in init we were to go back to just saving the dungeon, not the whole runner. These seem OK and easy to change.

Let’s see what the deal is with the GameRunner and its dungeon member.

function GameRunner:init(testX, testY)
    self.tileSize = 64
    self.tileCountX = testX or 85 -- if these change, zoomed-out scale 
    self.tileCountY = testY or 64 -- may also need to be changed.
    self.cofloater = Floater(50,25,4)
    self.dungeonLevel = 0
    self.requestNewLevel = false
    self.playerRoom = 1
    Bus:subscribe(self, self.createNewLevel, "createNewLevel")
    Bus:subscribe(self, self.darkness, "darkness")
    self.dungeon = nil -- created in defineDungeonBuilder
end

function GameRunner:defineDungeonBuilder(providedLevel)
    if providedLevel then
        self.dungeonLevel = providedLevel
    else
        self.dungeonLevel = self.dungeonLevel + 1
        if self.dungeonLevel > 4 then self.dungeonLevel = 4 end
    end
    self.dungeon = Dungeon(self, self.tileCountX, self.tileCountY)
    local numberOfMonsters = 6
    return DungeonBuilder(self,self.dungeon, self.dungeonLevel, self.playerRoom, numberOfMonsters)
end

Ah. There’s really just the one place where we save the dungeon into Runner. So why can’t the Tile, when created, save just the dungeon?

I’m going to try it again. There’s something here that I don’t understand, and I’ll allow myself a few minutes to learn, and possibly, to find the issue.

Ah. Before I even do much, I see the problem. The gameRunner doesn’t have the dungeon until after the creation returns, and the creation is creating the tiles:

function Dungeon:init(runner, tileCountX, tileCountY)
    self.runner = runner
    self.tileCountX = tileCountX or assert(false, "must provide tile count")
    self.tileCountY = tileCountY or assert(false, "must provide tile count")
    self:createTiles()
    self:clearLevel()
end

I’ve tried a few times to “explain” what’s going on, but I really can’t: I might be able to inspect a lot of code and draw a picture and finally see what is actually going on, but I think the essence is that the Dungeon is creating the tiles during its own creation, and the GameRunner doesn’t have a decent instance at this point.

I have an idea for something that might break things in an illuminating way:

function GameRunner:defineDungeonBuilder(providedLevel)
    if providedLevel then
        self.dungeonLevel = providedLevel
    else
        self.dungeonLevel = self.dungeonLevel + 1
        if self.dungeonLevel > 4 then self.dungeonLevel = 4 end
    end
    self.dungeon = nil -- clear out any old ones, hoping to cause a crash
    self.dungeon = Dungeon(self, self.tileCountX, self.tileCountY)
    local numberOfMonsters = 6
    return DungeonBuilder(self,self.dungeon, self.dungeonLevel, self.playerRoom, numberOfMonsters)
end

If I nil the dungeon, any leftover one from a prior level will be lost and we should get a traceback. Let’s see.

I really expected that to cause a crash. It didn’t. I add this:

function GameRunner:getDungeon()
    assert(self.dungeon, "attempt to get a nil dungeon")
    return self.dungeon
end

OK so far. Now this:

function Tile:init(mapPoint,kind, runner)
    assert(mapPoint:is_a(MapPoint), "Tile requires MapPoint")
    self.mapPoint = mapPoint
    self.kind = kind
    self.runner = runner
    self.dungeon = runner:getDungeon()
    self:initDetails()
end

Ah. The assertion fails:

GameRunner:190: attempt to get a nil dungeon
stack traceback:
	[C]: in function 'assert'
	GameRunner:190: in method 'getDungeon'
	Tile:93: in field 'init'
	... false
    end

    setmetatable(c, mt)
    return c
end:24: in local 'klass'
	BaseMap:33: in method 'createRectangle'
	BaseMap:25: in field 'init'
	... false
    end

    setmetatable(c, mt)
    return c
end:24: in function <... false
    end

    setmetatable(c, mt)
    return c
end:20>
	(...tail calls...)
	Dungeon:247: in method 'createTiles'
	Dungeon:123: in field 'init'
	... false
    end

    setmetatable(c, mt)
    return c
end:24: in global 'Dungeon'
	GameRunner:105: in method 'defineDungeonBuilder'
	Tests:22: in field '_before'
	CodeaUnit:44: in method 'test'
	Tests:36: in local 'allTests'
	CodeaUnit:16: in method 'describe'
	Tests:13: in function 'testDungeon2'
	[string "testDungeon2()"]:1: in main chunk
	CodeaUnit:139: in method 'execute'
	Main:13: in function 'setup'

This gives me confidence that I understand the issue, though the details are fuzzy. There’s no question now that Tiles are being initialized at a time when they cannot reasonably fetch the dungeon from the GameRunner. Can we move that part of initialization out of Dungeon:init?

function Dungeon:init(runner, tileCountX, tileCountY)
    self.runner = runner
    self.tileCountX = tileCountX or assert(false, "must provide tile count")
    self.tileCountY = tileCountY or assert(false, "must provide tile count")
    self:createTiles()
    self:clearLevel()
end

Let’s think about where we think we’re going. Ideally, GameRunner wouldn’t create the Dungeon instance: DungeonBuilder would. For that to happen, we would return the dungeon from the builder and only then save it in GameRunner. We’ve been unable to do that because there are references from Tile back to GameRunner, which need to refer to the dungeon, so that GameRunner needs to be able to refer back to the dungeon while it’s being built. Nasty.

Dilemma

I’m torn between options. One is just to move dungeon creation into DungeonBuilder, and allow it to return the Dungeon, either right from the build, or via an accessor. That will surely break a lot of things. Then we’d do a monkey-patch sort of thing to send the partially-created dungeon right back to the runner, where it would be tucked away for use by whoever’s calling up to runner. Then we could put traps in to find out who’s doing it and to fix them.

That’s a pretty big switch, though the initial change is probably fairly easy. It’s just that it may break a lot.

Another option would be to lazy-init the Dungeon, creating the tiles, not in the init, but later, when we go to build it. With this scheme, we’d let GameRunner create it, for now, and all would work well enough, and we could complete the unwiring of Tile (I think).

In a situation like this, we really need a solution for the gripping hand. Two ideas just aren’t enough ideas. But I just have the two.

Let’s try the lazy init. It should be harmless enough, at least if it’s easy.

Let’s change this:

function Dungeon:init(runner, tileCountX, tileCountY)
    self.runner = runner
    self.tileCountX = tileCountX or assert(false, "must provide tile count")
    self.tileCountY = tileCountY or assert(false, "must provide tile count")
    self:createTiles()
    self:clearLevel()
end

function DungeonBuilder:init(runner, dungeon, levelNumber, playerRoomNumber, numberOfMonsters)
    self.RUNNER = runner
    self.dungeon = dungeon
    self.levelNumber = levelNumber
    self.playerRoomNumber = playerRoomNumber
    self.numberOfMonsters = numberOfMonsters
end

To this:

function Dungeon:init(runner, tileCountX, tileCountY)
    self.runner = runner
    self.tileCountX = tileCountX or assert(false, "must provide tile count")
    self.tileCountY = tileCountY or assert(false, "must provide tile count")
end

function DungeonBuilder:init(runner, dungeon, levelNumber, playerRoomNumber, numberOfMonsters)
    self.RUNNER = runner
    self.dungeon = dungeon
    self.levelNumber = levelNumber
    self.playerRoomNumber = playerRoomNumber
    self.numberOfMonsters = numberOfMonsters
    self.dungeon:createTiles()
    self.dungeon:clearLevel()
end

I rather expect this to work. Let’s find out. Tests and game run. Perfect. Commit: Dungeon Tile creation triggered by DungeonBuilder, not in Dungeon:init.

That seems reasonable. But can we now change Tile to cache the dungeon and not the runner? I think perhaps we can. Let’s try it.

function Tile:init(mapPoint,kind, runner)
    assert(mapPoint:is_a(MapPoint), "Tile requires MapPoint")
    self.mapPoint = mapPoint
    self.kind = kind
    --self.runner = runner
    self.dungeon = runner:getDungeon()
    self:initDetails()
end

function Tile:getDungeon()
    return self.dungeon
end

Test, feeling less than confident. This has broken so many times before. Tests run, game runs. Woot! Commit: Tile no longer saves runner instance!

Let’s reflect. This may not be quite perfect (it never is, but we might do better).

What Have We Wrought?

We have this:

function DungeonBuilder:init(runner, dungeon, levelNumber, playerRoomNumber, numberOfMonsters)
    self.RUNNER = runner
    self.dungeon = dungeon
    self.levelNumber = levelNumber
    self.playerRoomNumber = playerRoomNumber
    self.numberOfMonsters = numberOfMonsters
    self.dungeon:createTiles()
    self.dungeon:clearLevel()
end

Now it turns out that when we create a DungeonBuilder, we then tell it what to build:

function GameRunner:createLearningLevel()
    local builder = self:defineDungeonBuilder(99)
    builder:buildLearningLevel()
    self:prepareToRun()
end

function GameRunner:createLevel(roomCount)
    local builder = self:defineDungeonBuilder()
    builder:buildLevel(roomCount)
    self:prepareToRun()
end

function GameRunner:createTestLevel(roomCount)
    local builder = self:defineDungeonBuilder()
    builder:buildTestLevel(roomCount)
    -- self:prepareToRun() -- probably not needed
end

All those build methods look similar:

function DungeonBuilder:buildLevel(roomCount)
    self:dcPrepare()
    self:defineDungeonLayout(roomCount)
    self:placePlayer()
    self:customizeContents()
    self:dcFinalize()
    return self.dungeon
end

function DungeonBuilder:buildTestLevel(roomCount)
    self:dcPrepare()
    self:defineDungeonLayout(roomCount)
    self:dcFinalize()
    return self.dungeon
end

function DungeonBuilder:buildLearningLevel()
    self:dcPrepare()
    self:defineLearningLayout()
    self:placePlayer()
    self:customizeLearningContents()
    self:dcFinalize()
    return self.dungeon
end

They all start with dcPrepare. Perhaps it would be better to create and clear there. Let’s try that:

function DungeonBuilder:init(runner, dungeon, levelNumber, playerRoomNumber, numberOfMonsters)
    self.RUNNER = runner
    self.dungeon = dungeon
    self.levelNumber = levelNumber
    self.playerRoomNumber = playerRoomNumber
    self.numberOfMonsters = numberOfMonsters
end

function DungeonBuilder:dcPrepare()
    TileLock = false
    self.dungeon:createTiles()
    self.dungeon:clearLevel()
    DungeonContents = DungeonContentsCollection()
    MonsterPlayer(self.RUNNER)
    Announcer:clearCache()
end

Test. That works. Commit: defer dungeon level creation and clearing to DungeonBuilder:dcPrepare.

You may be wondering why I think that’s better. It’s because the creation and clearing of tiles is now closer in timeto the other builder aspects of dungeon building, so that less can go wrong between those events. In addition, if by accident we held on to an old dungeon and built on it a second time, its tiles would be recreated and cleared again, which would ensure nothing was left over from last time. Now we don’t expect ever to do that, because it would obviously be a mistake, but I find it best to arrange that obvious mistakes are less likely to happen, because, oddly enough, I make mistakes all the time that should have been obvious.

Something about being human, at least according to the code that causes me to simulate being human.

I think this is a marvelous time to sum up.

Summary

We started with the card that said we should change Tile so that it only refers to the dungeon rather than retaining a runner. It now does that. Along the way, we encountered one of those bugs you can’t explain, but you know what’s causing it. There was some interaction between gameRunner and Tile such that at the time Tiles were created, the GameRunner didn’t know the Dungeon yet, so that saving it wouldn’t work, while accessing it indirectly later would.

We fixed that by deferring the creation of Tiles until after the initial dungeon was returned to GameRunner.

We are left, however, with GameRunner creating the dungeon and then asking DungeonBuilder to fill it in. That should be changed so that DB creates the dungeon and returns it.

That’s for another day.

Over the course of a few hours of coding and mostly writing, we have improved the Tile object, improved the relationship between GameRunner, Dungeon, and Tile, and made the world a little bit better. We did it in ten commits, a rate of about every fifteen minutes. Not bad.

Along the way, we took a bite that was much too large, and calmed our keys and starting doing much smaller steps. That helped us (me) focus on single changes at a time, and ensured that every little bit of progress was locked in before trying something new.

Overall, a decent result. As as so often happens, things did not go quite down the path we foresaw, but they did go mostly rather well.

What’s next? I can’t wait to find out.