More gentle refactoring in the general direction of DungeonBuilder. No Monty Python reference intended.

It feels to me as if I’ve finally got reasonably stable ground to stand upon. I think moving the building code over to DungeonBuilder should go fairly smoothly, although I am sure we’ll find some trouble somewhere. I’ll try to be clear about what I’m thinking as I select things to do.

Of course, we’ll begin with a review. It has been literally hours since we last worked on this.

DungeonBulder looks like this so far:

DungeonBuilder = class()

function DungeonBuilder:init(runner, dungeon)
    self.runner = runner
    self.dungeon = dungeon
end

function DungeonBuilder:buildLevel(roomCount)
    self:dcPrepare()
    self.runner:buildLevel(roomCount)
end

function DungeonBuilder:dcPrepare()
    TileLock = false
    DungeonContents = DungeonContentsCollection()
    MonsterPlayer(self.runner)
    Announcer:clearCache()
end

In buildLevel, we have a start at the top-level building code. Just now it only does two things, our own dcPrepare and a call back to GameRunner’s buildLevel. Things start here:

function GameRunner:createLevel(count)
    self:createNewDungeon()
    self.builder:buildLevel(count)
end

function GameRunner:buildLevel(count)
    -- determine level
    self.dungeonLevel = self.dungeonLevel + 1
    if self.dungeonLevel > 4 then self.dungeonLevel = 4 end
    
    -- customize rooms and connections
    self.dungeon:createRandomRooms(count)
    self.dungeon:connectRooms(self:getRooms())
    
    -- paint dungeon correctly
    self.dungeon:convertEdgesToWalls()
    
    self:placePlayerInRoom1()
    
    -- customize contents
    self:placeWayDown()
    self:placeSpikes(15)
    self:placeLever()
    --self:placeDarkness()
    self:placeNPC()
    self:setupMonsters(6)
    self.keys = self:createThings(Key,5)
    self:createThings(Chest,5)
    self:createLoots(10)
    self:createDecor(30)
    self:createButtons()
    
    -- prepare crawl
    self.cofloater:startCrawl()
    Announcer:sayAndSave(self:crawlMessages(self.dungeonLevel))
    self:startTimers()
    
    self:dcFinalize()
end

GameRunner is asked to createLevel and it creates a new Dungeon, then calls in to DungeonBuilder, which soon calls back, and GameRunner still does most of the work. We are here to move that work over to the builder.

Roughly what I plan to do is to separate the GameRunner’s buildLevel into smaller functions, dividing them up such that one of them can be easily moved to the builder. Rinse, repeat.

These three lines seem promising:

    -- customize rooms and connections
    self.dungeon:createRandomRooms(count)
    self.dungeon:connectRooms(self:getRooms())
    
    -- paint dungeon correctly
    self.dungeon:convertEdgesToWalls()

They are all sending messages to dungeon, so they should be able to be sent just as well from the builder. But there is the issue of the rooms. Let’s look in a bit more detail:

function Dungeon:createRandomRooms(count)
    local r
    self.rooms = {}
    while count > 0 do
        count = count -1
        local timeout = 100
        local placed = false
        while not placed do
            timeout = timeout - 1
            r = Room:random(self.tileCountX,self.tileCountY, 5,14, self.runner)
            if r:isAllowed(self) then
                placed = true
                table.insert(self.rooms,r)
                r:paint(#self.rooms)
            elseif timeout <= 0 then
                placed = true
            end
        end
    end
    return self.rooms
end

Good news. The dungeon has the rooms and the GameRunner fetches them when it wants them. I can see no reason why we would pass in the rooms to the next method, connectRooms but we’ll continue the practice for now.

We are making the simplest changes we can find, slowly moving things over into DungeonBuilder. Accordingly, I’m trying to make only changes that I’m sure can’t change behavior, and I’m going to ignore opportunities for general improvement. I want to be careful.

Anyway we can surely refactor the GameRunner’s buildLevel this way:

function GameRunner:buildLevel(count)
    self:buildLevelA(count)
    self:buildLevelB(count)
    self:buildLevelC(count)
end

And divide up the original buildLevel thus:

function GameRunner:buildLevelA(count)
    -- determine level
    self.dungeonLevel = self.dungeonLevel + 1
    if self.dungeonLevel > 4 then self.dungeonLevel = 4 end
end

function GameRunner:buildLevelB(count)
    -- customize rooms and connections
    self.dungeon:createRandomRooms(count)
    self.dungeon:connectRooms(self:getRooms())
    
    -- paint dungeon correctly
    self.dungeon:convertEdgesToWalls()
end

function GameRunner:buildLevelC(count)
    self:placePlayerInRoom1()
    
    -- customize contents
    self:placeWayDown()
    self:placeSpikes(15)
    self:placeLever()
    --self:placeDarkness()
    self:placeNPC()
    self:setupMonsters(6)
    self.keys = self:createThings(Key,5)
    self:createThings(Chest,5)
    self:createLoots(10)
    self:createDecor(30)
    self:createButtons()
    
    -- prepare crawl
    self.cofloater:startCrawl()
    Announcer:sayAndSave(self:crawlMessages(self.dungeonLevel))
    self:startTimers()
    
    self:dcFinalize()
end

These could perhaps be given better names but I consider them temporary.

The game should work now. Test. We’re good. Commit: separate GameRunner:buildLevel into three separate methods in prep for moving some function to DungeonBuilder.

Now we can surely make DungeonBuilder to level B:

function GameRunner:buildLevelB(count)
    self.builder:buildLevelB(count)
end

function DungeonBuilder:buildLevelB(count)
    -- customize rooms and connections
    self.dungeon:createRandomRooms(count)
    self.dungeon:connectRooms(self.rooms)
    
    -- paint dungeon correctly
    self.dungeon:convertEdgesToWalls()
end

Note that I remembered to refer to self.rooms in there. Arguably, now that I think about it, I could have left it as is and “discovered” that I need the accessor. Shall we do that? Let’s pretend that I didn’t make that change:

function DungeonBuilder:buildLevelB(count)
    -- customize rooms and connections
    self.dungeon:createRandomRooms(count)
    self.dungeon:connectRooms(self:getRooms())
    
    -- paint dungeon correctly
    self.dungeon:convertEdgesToWalls()
end

Now when we run, we should get an error on getRooms Anything else will surprise me.

DungeonBuilder:49: attempt to call a nil value (method 'getRooms')
stack traceback:
	DungeonBuilder:49: in method 'buildLevelB'
	GameRunner:155: in method 'buildLevelB'
	GameRunner:144: in method 'buildLevel'
	DungeonBuilder:43: in method 'buildLevel'
	GameRunner:139: in method 'createLevel'
	Main:64: in function 'setup'

Oops, forgot to build getRooms. Oh, and the other change wouldn’t have even worked, would it? No. :)

function DungeonBuilder:getRooms()
    return self.dungeon:getRooms()
end

Will that suffice? Don’t wonder, test. No, Dungeon doesn’t understand getRooms.

function Dungeon:getRooms()
    return self.rooms
end

I suspect that GameRunner has getRooms in a form that we don’t like. Right:

function GameRunner:getRooms()
    return self.dungeon.rooms
end

Now this time I’m going to fix this, because it is part of the evolving design. When an accessor exists, I feel that it should always be used.

function GameRunner:getRooms()
    return self.dungeon:getRooms()
end

I think I’d like to see if this is used. And it is, rather often. Probably some feature envy going on. But we’re after different game here. I think we should be working now. Tests indicate that we are. Commit: moved dungeon room creation, connection, and painting to DungeonBuilder.

Now let’s improve a name or two here and perhaps change things around. First the rearrangement.

Now that there are three levelA B C methods in GameRunner, we can do this:

function DungeonBuilder:buildLevel(roomCount)
    self:dcPrepare()
    self.runner:buildLevelA(roomCount)
    self:buildLevelB(roomCount)
    self.runner:buildLevelC(roomCount)
end

Now we can remove buildLevelB from GameRunner. All should still be well. It is. One more thing before commit:

function DungeonBuilder:buildLevel(roomCount)
    self:dcPrepare()
    self.runner:buildLevelA(roomCount)
    self:defineDungeonLayout(roomCount)
    self.runner:buildLevelC(roomCount)
end

function DungeonBuilder:defineDungeonLayout(count)
    -- customize rooms and connections
    self.dungeon:createRandomRooms(count)
    self.dungeon:connectRooms(self:getRooms())
    
    -- paint dungeon correctly
    self.dungeon:convertEdgesToWalls()
end

defineDungeonLayout seems like a better name than “B”. Commit: Refactor DungeonBuilder slightly.

Now what have we left?

function GameRunner:buildLevel(count)
    self:buildLevelA(count)
    self:buildLevelB(count)
    self:buildLevelC(count)
end

function GameRunner:buildLevelA(count)
    -- determine level
    self.dungeonLevel = self.dungeonLevel + 1
    if self.dungeonLevel > 4 then self.dungeonLevel = 4 end
end

function GameRunner:buildLevelC(count)
    self:placePlayerInRoom1()
    
    -- customize contents
    self:placeWayDown()
    self:placeSpikes(15)
    self:placeLever()
    --self:placeDarkness()
    self:placeNPC()
    self:setupMonsters(6)
    self.keys = self:createThings(Key,5)
    self:createThings(Chest,5)
    self:createLoots(10)
    self:createDecor(30)
    self:createButtons()
    
    -- prepare crawl
    self.cofloater:startCrawl()
    Announcer:sayAndSave(self:crawlMessages(self.dungeonLevel))
    self:startTimers()
    
    self:dcFinalize()
end

I foresee that we are going to need the dungeon level as we ove things over, but we’ll deal with that as needed. Let’s divide up buildLevelC into its three rather obvious parts:

function GameRunner:buildLevelC()
    self:placePlayer()
    self:customizeContents()
    self:prepareCrawlAndTimers()
    self:dcFinalize()
end

function GameRunner:placePlayer()
    self:placePlayerInRoom1()
end

function GameRunner:customizeContents()
    -- customize contents
    self:placeWayDown()
    self:placeSpikes(15)
    self:placeLever()
    --self:placeDarkness()
    self:placeNPC()
    self:setupMonsters(6)
    self.keys = self:createThings(Key,5)
    self:createThings(Chest,5)
    self:createLoots(10)
    self:createDecor(30)
    self:createButtons()
end

function GameRunner:prepareCrawlAndTimers()
    -- prepare crawl
    self.cofloater:startCrawl()
    Announcer:sayAndSave(self:crawlMessages(self.dungeonLevel))
    self:startTimers()
end

I feel that I’m accomplishing three things here:

  1. I’m going Very Carefully;
  2. I’m producing meaningful smaller methods;
  3. I’m identifying bits that can move over to DungeonBuilder.

It is quite likely that I could go in bigger chunks. I am reluctant to do that. Two reasons: First, I’ve been badly burned last Sunday going in bigger chunks. Second, smaller chunks only feel slow. Since when they break the trouble is more obvious, they avoid long head-scratching debugging sessions.

Of course, you need to pic your own chunk size. I would urge you to try very small ones and develop a feeling for how small is too small … if it ever is.

Let’s make sure this works and commit it: Break down building into smaller methods.

OK, what shall we move?

Let’s pick an obvious one, the one with just one method in it:

function GameRunner:placePlayer()
    self:placePlayerInRoom1()
end

Now I could move that over and call back. And now that I’ve thought of it, I will, even though it does feel like only a half a step.

Let’s do it this way:

function GameRunner:buildLevelC()
    self.builder:placePlayer()
    self:customizeContents()
    self:prepareCrawlAndTimers()
    self:dcFinalize()
end

function DungeonBuilder:placePlayer()
    self.runner:placePlayerInRoom1()
end

I can remove the placePlayer from GameRunner. Test, commit: moved placePlayer to DungeonBuilder

Now let’s look and see what the function actually does in GameRunner. Ah, it’s a bit intricate:

function GameRunner:placePlayerInRoom1()
    local r1 = self:getRooms()[1]
    local tile = r1:centerTile()
    self.player = Player:cloneFrom(self.player, tile,self)
end

We can certainly do the first two elsewhere. But we need to clone a player from the existing one, and save it in GameRunner. I think we’d like to let GameRunner continue to deal with the player aspect, and provide the needed tile information from the builder.

I’m not sure. One possibility is to do this:

function DungeonBuilder:placePlayer()
    local r1 = self:getRooms()[1]
    local tile = r1:centerTile()
    self.runner:placePlayerInRoom1(tile)
end

function GameRunner:placePlayerInRoom1(tile)
    self.player = Player:cloneFrom(self.player, tile,self)
end

We compute the proper tile, then tell the GameRunner where to put her. We’ll fix the names shortly. Let’s be sure it works. It does. Rename the method:

function GameRunner:placePlayer(tile)
    self.player = Player:cloneFrom(self.player, tile,self)
end

function DungeonBuilder:placePlayer()
    local r1 = self:getRooms()[1]
    local tile = r1:centerTile()
    self.runner:placePlayer(tile)
end

I find that there are other callers to the old method, so I’ll recreate it for them.

function GameRunner:placePlayerInRoom1()
    local r1 = self:getRooms()[1]
    local tile = r1:centerTile()
    self:placePlayer(tile)
end

I do call the new one, avoiding duplicating that clone call. I think I could actually cause the other users to call to the builder, but that seems out of scope. Test. Commit: DungeonBuilder provides starting tile to GameRunner:placePlayer.

Let’s do a RESTrospective.

RESTrospective

I had to think a bit harder than I wanted to on that last bit. the GameRunner does want to know the player, however, and the dungeon builder really doesn’t. So it makes sense that there’d be some kind of arrangement. I’m not entirely sure about this, though.

It might be better for the player positioning to wait until the builder is gone and then fetch the needed info from the Dungeon. Maybe we should make a card for that: it might be simpler overall, rather than having the callback.

For now, let’s go forward. But I haven’t had my banana and chai yet, so take a break, we’ll begin again in a few minutes.

Here We Go

Part of me feels that there should be a “design” here. In fact, there perhaps should have been a builder-user design from the very beginning, but at the time I didn’t see it and you didn’t tell me. We’ll call it even.

What I’m missing is a coherent sense of what goes where, who uses it, and so on. And that is partly due tot he fact that there really aren’t a few simple centralized objects here. Should there be? Sure, I’d prefer it, or think I would. Could there be? Intuitively, yes, but I do observe that there’s a lot going on in the game, so my intuition may be misleading me.

Should I do one now? I don’t think so. It might be profitable to look a bit more forward and a bit more backward, to see if there’s some object waiting to be discovered, but that feeling is, I think, mistaken.

In a real situation with this code, we would not have much chance of a big redesign refactoring. We would have to make things better incrementally over time. As such, we have to edge out way into a good design, we can’t just create one by fiat.

But we’ll try to learn lessons as we go, and try to incorporate them. Let’s get to it.

Building the level now starts in DungeonBuilder:

function DungeonBuilder:buildLevel(roomCount)
    self:dcPrepare()
    self.runner:buildLevelA(roomCount)
    self:defineDungeonLayout(roomCount)
    self.runner:buildLevelC(roomCount)
end

In GameRunner, level C is this:

function GameRunner:buildLevelC()
    self.builder:placePlayer()
    self:customizeContents()
    self:prepareCrawlAndTimers()
    self:dcFinalize()
end

function GameRunner:customizeContents()
    -- customize contents
    self:placeWayDown()
    self:placeSpikes(15)
    self:placeLever()
    --self:placeDarkness()
    self:placeNPC()
    self:setupMonsters(6)
    self.keys = self:createThings(Key,5)
    self:createThings(Chest,5)
    self:createLoots(10)
    self:createDecor(30)
    self:createButtons()
end

And so on. Let’s look at the individual methods in customizeContents and see whether any are easy, and whether any patterns emerge.

function GameRunner:placeWayDown()
    local r1 = self:getRooms()[1]
    local target = r1:centerTile()
    local tile = self:getDungeon():farawayOpenRoomTile(r1, target)
    self.wayDown = WayDown(tile)
end

This is another one where the GameRunner wants to know where the WayDown is. Since I’m not entirely happy with the callback we did for player, let’s defer that one. Some others:

function GameRunner:placeSpikes(count)
    for i = 1,count or 20 do
        local tile = self:getDungeon():randomHallwayTile()
        Spikes(tile)
    end
end

function GameRunner:placeNPC()
    local r1 = self:getRooms()[1]
    local tile = r1:centerTile()
    local npcTile = tile:getNeighbor(vec2(2,0))
    NPC(npcTile)
end

function GameRunner:placeDarkness()
    local r1 = self:getRooms()[1]
    local tile = r1:centerTile()
    local darkTile = tile:getNeighbor(vec2(-2,0))
    local dark = Darkness(darkTile)
end

function GameRunner:placeLever()
    local r1 = self:getRooms()[1]
    local tile = r1:centerTile()
    local leverTile = tile:getNeighbor(vec2(0,2))
    local lever = Lever(leverTile, "Lever 1")
end

Ah. A lot of these appear not to care where they are done. Let’s see about moving one over. Maybe the spikes.

I’ll move the WayDown creation Way Down in customize:

function GameRunner:customizeContents()
    self:placeSpikes(15)
    self:placeLever()
    --self:placeDarkness()
    self:placeNPC()
    self:setupMonsters(6)
    self.keys = self:createThings(Key,5)
    self:createThings(Chest,5)
    self:createLoots(10)
    self:createDecor(30)
    self:placeWayDown()
    self:createButtons()
end

Buttons seem special, so I left those to last. Test just to be sure moving the WayDown doesn’t cause problems. It does. Has to be done before setupMonsters, because a couple of then go near the WayDown. I’ll move setupMonsters down as well, see how that goes.

function GameRunner:customizeContents()
    self:placeSpikes(15)
    self:placeLever()
    --self:placeDarkness()
    self:placeNPC()
    self.keys = self:createThings(Key,5)
    self:createThings(Chest,5)
    self:createLoots(10)
    self:createDecor(30)
    self:placeWayDown()
    self:setupMonsters(6) -- must follow waydown
    self:createButtons()
end

That seems good. Commit: reorder customizeContents.

Now I want to do the spikes in DungeonBuilder. It’ll take a few steps to do this:

function GameRunner:buildLevelC()
    self.builder:placePlayer()
    self:customizeContents()
    self:prepareCrawlAndTimers()
    self:dcFinalize()
end

Let’s first move this over entirely.

function DungeonBuilder:buildLevel(roomCount)
    self:dcPrepare()
    self.runner:buildLevelA(roomCount)
    self:defineDungeonLayout(roomCount)
    self:buildLevelC(roomCount)
end

function DungeonBuilder:buildLevelC()
    self:placePlayer()
    self.runner:customizeContents()
    self.runner:prepareCrawlAndTimers()
    self.runner:dcFinalize()
end

I expect this to work, but I was distracted in the middle so I’m more interested than usual in the tests. We’re good. Commit: move level C into DungeonBuilder, call back to GameRunner.

Now I think I’ll put a customizeContents into DungeonBuilder, and move things one at a time, calling both until the GameRunner one is empty:

function DungeonBuilder:buildLevelC()
    self:placePlayer()
    self:customizeContents()
    self.runner:customizeContents()
    self.runner:prepareCrawlAndTimers()
    self.runner:dcFinalize()
end

function DungeonBuilder:customizeContents()
end

This has no effect. Test and commit anyway: add customizeContents to DungeonBuilder, to received moved methods.

Step By Step

Now we should be able to move the “easy ones” over. I’ll start with Spikes:

function GameRunner:customizeContents()
    self:placeSpikes(15) -- DELETE THIS
    self:placeLever()
    --self:placeDarkness()
    self:placeNPC()
    self.keys = self:createThings(Key,5)
    self:createThings(Chest,5)
    self:createLoots(10)
    self:createDecor(30)
    self:placeWayDown()
    self:setupMonsters(6) -- must follow waydown
    self:createButtons()
end

function DungeonBuilder:customizeContents()
    self:placeSpikes(15) -- added
end

function DungeonBuilder:placeSpikes(count)
    for i = 1,count or 20 do
        local tile = self.dungeon:randomHallwayTile()
        Spikes(tile)
    end
end

The last method is moved and modified to refer to self.dungeon. I expect this to work. And it does. Commit: spikes created in DungeonBuilder.

Continuing …

function DungeonBuilder:customizeContents()
    self:placeSpikes(15)
    self:placeLever()
end

function DungeonBuilder:placeLever()
    local r1 = self:getRooms()[1]
    local tile = r1:centerTile()
    local leverTile = tile:getNeighbor(vec2(0,2))
    Lever(leverTile, "Lever 1")
end

Test. Commit: lever created in DungeonBuilder.

I’ll continue these until I hit a problem or an interesting one. I plan to do them one at a time with a commit each time. Anything else just invites trouble.

LOL. First mistake: move the method but don’t change the class name. Quickly discovered. Moving right along, nothing to see here …

Next this got semi-interesting:

function DungeonBuilder:placeLoots(n)
    for i =  1, n or 1 do
        local tab = RandomLootInfo[math.random(1,#RandomLootInfo)]
        local tile = self:randomRoomTile(self.playerRoom)
        Loot(tile, tab[1], tab[2], tab[3])
    end
end

I have to find and more RandomLootInfo. I’m going to improve that in a moment. Then I discover that I don’t have randomRoomTile here.

function GameRunner:randomRoomTile(roomNumberToAvoid)
    local room = nil
    if roomNumberToAvoid < #self:getRooms() then
        room = self:getRooms()[roomNumberToAvoid]
    end
    return self:getDungeon():randomRoomTile(room)
end

This is really odd. What does randomRoomTile do?

function Dungeon:randomRoomTile(roomToAvoid)
    local x1,y1, x2,y2 = 0,0,0,0
    if roomToAvoid then
        x1,y1, x2,y2 = roomToAvoid:corners()
    end
    return self:randomRoomTileAvoiding(x1,y1, x2,y2)
end

OK, we can do this. The name is bad above though:

I’m going to leave the original method in GameRunner: I think it is used in places we haven’t improved yet.

Here’s what works:

function DungeonBuilder:placeLoots(n)
    for i =  1, n or 1 do
        local tab = RandomLootInfo[math.random(1,#RandomLootInfo)]
        local tile = self:randomRoomTile(1)
        Loot(tile, tab[1], tab[2], tab[3])
    end
end

function DungeonBuilder:randomRoomTile(roomNumberToAvoid)
    local avoidedRoom = nil
    if roomNumberToAvoid < #self:getRooms() then
        avoidedRoom = self:getRooms()[roomNumberToAvoid]
    end
    return self.dungeon:randomRoomTile(avoidedRoom)
end

I had to call randomRoomTile with an explicit 1, the player room. Let’s comment that for later improvement.

function DungeonBuilder:placeLoots(n)
    for i =  1, n or 1 do
        local tab = RandomLootInfo[math.random(1,#RandomLootInfo)]
        local tile = self:randomRoomTile(1) -- MAGIC NUMBER PLAYER ROOM
        Loot(tile, tab[1], tab[2], tab[3])
    end
end

We’re good. Commit: Loots created in DungeonBuilder.

Continuing …

Here’s where I stand now:

function GameRunner:customizeContents()
    self.keys = self:createThings(Key,5)
    self:createThings(Chest,5)
    self:placeWayDown()
    self:setupMonsters(6) -- must follow waydown
    self:createButtons()
end

The others all moved over in the obvious way. I was troubled by this one, as you may recall, because of the need to store the keys. So I checked the code and no one references the keys collection.

So I’ll move both of those together:

function DungeonBuilder:customizeContents()
    self:placeSpikes(15)
    self:placeLever()
    --self:placeDarkness()
    self:placeNPC()
    self:placeLoots(10)
    self:createDecor(30)
    self:createThings(Key,5)
    self:createThings(Chest,5)
end

function GameRunner:createThings(aClass, n)
    for i = 1,n or 1 do
        local tile = self:randomRoomTile(self.playerRoom)
        aClass(tile,self)
    end
end

I expect this to work. It will as soon as I change the signature above. Second tine I’ve made that mistake today.

function DungeonBuilder:createThings(aClass, n)
    for i = 1,n or 1 do
        local tile = self:randomRoomTile(self.playerRoom)
        aClass(tile,self)
    end
end

My hopes are daunted.

DungeonBuilder:159: attempt to compare nil with number
stack traceback:
	DungeonBuilder:159: in method 'randomRoomTile'
	DungeonBuilder:152: in method 'createThings'
	DungeonBuilder:71: in method 'customizeContents'
	DungeonBuilder:58: in method 'buildLevelC'
	DungeonBuilder:53: in method 'buildLevel'
	GameRunner:139: in method 'createLevel'
	Main:64: in function 'setup'

Oh, there’s that player room again. Let’s fix it:

function DungeonBuilder:init(runner, dungeon)
    self.runner = runner
    self.dungeon = dungeon
    self.playerRoom = 1 -- could this change? If so, make param.
end

function DungeonBuilder:placeLoots(n)
    for i =  1, n or 1 do
        local tab = RandomLootInfo[math.random(1,#RandomLootInfo)]
        local tile = self:randomRoomTile(self.playerRoom)
        Loot(tile, tab[1], tab[2], tab[3])
    end
end

Test. We’re good. Commit: Keys and Chests created in DungeonBuilder.

It’s 1130 hours and time to stop. Let’s sum up.

Summary

We’ve made excellent progress this morning. This is, I think, a sign that we’re on the right track. Once we get a reasonable setup, most things move fairly easily. Counting writing time as well as programming and testing time, the individual moves took between ten and twenty minutes, and would be must shorter than that with a more powerful IDE and without writing an article as one goes.

I tried to commit after each step, and did pretty well. 15 commits between 0900 and 1130, six per hour if I’ve done that right. Not too bad.

We have probably 90 percent of the main create level moved over and moving the other creations should be easy. The remaining ten percent looks a bit more tricky, but shouldn’t require anything exotic.

My biggest concern is to find a good way of handling the specialized information that GameRunner needs, at least the player information. We’ll look at that and decide. It’s working now, it’s just a bit indirect.

Good session. Thanks for your help. See you next time!