Back to tiny steps, if I possibly can. And you know what? I bet I can.

Yesterday I spent a long time between commits. Things were always under control, with brief moments of concern, but had I run out of time, I’d have had to revert my work, or stash it, or one of those Git things, because I had intentionally broken the program and was ticking through the steps to fix it.

Hm, FTP to my site is failing with error 203. Help request sent. I hope that one day you may see this article.

Anyway, what we accomplished yesterday was to build a new object, BuilderView, that is a Façade for Dungeon, providing just the minimal functions that DungeonBuilder needs to access in Dungeon. My intention was, and is, to reduce the complexity of the system, making these builder changes easier.

Looking forward, I recall that there are a lot of building calls made back to GameRunner, and, in addition, there are a large number of calls made from things in the Dungeon back to Runner, which may suggest making additional views of GameRunner. For folks using languages with the Interface notion, these façades work as Interfaces, at the cost of an additional message dispatch, and with the possible advantage of a bit of transformation in the façade, in the manner of an Adapter pattern. I don’t think we’ve done that yet but it was a long time ago, yesterday, so I may have forgotten.

Anyway, let’s get back to moving things into Builder from Dungeon.

Moving Little Bits

The way I find things to move is simple. I look in DungeonBuilder for self.dungeon When I find one of those, it’s an opportunity to move something.

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

This one is legit. We put it in yesterday: when we return a constructed Dungeon, the caller wants the full interface. That call transforms our Façade back to a Dungeon. That occurs three times, for the three different dungeon builds we can perform.

function DungeonBuilder:clearLevel()
    for key, tile in self.dungeon:map():pairs() do
        tile:convertToEdge()
    end
end

This is OK but not great. This method shouldn’t really need to know the map, and ideally Builder wouldn’t know about it at all. (We may not be able to stick with that entirely, because we do have to get the map built somehow. But here, we can do this:

function DungeonBuilder:clearLevel()
    for key, tile in self.dungeon:tilePairs() do
        tile:convertToEdge()
    end
end

function BuilderView:tilePairs()
    return self.dungeon.map:pairs()
end

That should work. It does. Can we remove BuilderView:map? If we change this as well:

function DungeonBuilder:convertEdgesToWalls()
    for key,tile in self.dungeon:tilePairs() do
        tile:convertEdgeToWall()
    end
end

Now we can remove the :map() from BuilderView and test again. Test. Works. Commit: Replace calls to BuilderView:map with tilePairs. Remove map.

Continuing the search:

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

This is righteous, we’re just fetching things we actually need. Here’s the next one, and it’s an opportunity:

function DungeonBuilder:createLearningRooms()
    local w = 12
    local h =  8
    local a2 = { "In this room, there is a health power-up.",
        "Please walk next to it (not onto it)",
        "and press the ?? button.",
    "You'll get an informative message about whatever you're next to." }
    local t = {
        {2,2, w,h},
        {15,2, w,h, a2},
        {28,2, w,h},
        {41,2, w,h},
        {2,11, w,h},
        {15,11, w,h},
        {28,11, w,h},
        {41,11, w,h},
        {2,20, w,h},
        {15,20, w,h},
        {28,20, w,h},
        {41,20, w,h},
    }
    for i,r in ipairs(t) do
        r[1] = r[1]+24
        r[2] = r[2]+24
    end
    self.dungeon:createRoomsFromXYWHA(t, self.RUNNER)
    local r2 = self:getRooms()[2]
    local lootTile = r2:tileAt(2,2)
    local loot = Loot(lootTile, "health", 5,5)
    loot:setMessage("This is a Health Power-up of 5 points.\nStep onto it to add it to your inventory.")
end

The createRoomsFromXYWHA is just the sort of thing we’re trying to move over. I’ll bring a copy over into Builder and see what it does. Moved but not edited, it looks like this:

function Dungeon:createRoomsFromXYWHA(roomTable, runner)
    local makeRoom = function(result,roomDef)
        return result + Room:fromXYWHA(roomDef, runner:tileFinder())
    end
    ar.reduce(roomTable, self.rooms, makeRoom)
end

The only thing I see there that needs help is the self.rooms. We need to process the actual DungeonRooms table … that ar.reduce is putting rooms into it. We’ll do this:

function DungeonBuilder:createRoomsFromXYWHA(roomTable, runner)
    local makeRoom = function(result,roomDef)
        return result + Room:fromXYWHA(roomDef, runner:tileFinder())
    end
    ar.reduce(roomTable, self.dungeon:rooms(), makeRoom)
end

And in the BuilderView … I find getRooms already. I’ll use that instead. I have high hopes for this. Test to find out why I’m mistaken. The learning level builds. Remove the method from Dungeon and test again. All good. Commit: Move createRoomsFromXYWHA from Dungeon to Builder.

Find another self.dungeon:. … There are a lot of righteous references to self.dungeon now. They are irritating, because Codea’s “Find” capability is very last century. I’ll do a quick change. I’ll save two copies of my input BuilderView, one called view and one dungeon. I’ll change all the righteous references to view and they’ll disappear from … well, view.

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

Immediately I don’t entirely like this idea, because there are three cases, not just two.

  1. Things in DungeonBuilder that point to code in dungeon that needs to be changed.
  2. Legitimate “final” references through the view, which should probably all point to pure data when we’re done.
  3. Things in BuilderView that themselves refer to things we may want to move over.

An example of the third case:

function BuilderView:randomRoomTile()
    return self.dungeon:randomRoomTile()
end

Assuming that Dungeon doesn’t need randomRoomTile during game play, that method should move. So I’ve been a bit careful with which ones I change to self.view, trying to hit only things that I think are done-done.

Anyway, back to work. First test and commit Two copies of builderView, one called dungeon, used to mar things that change, one “view” for things that are done.

Now back to work. Let’s see about this:

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

Ah. The createTiles method might be tricky. What does it look like in situ?

function Dungeon:createTiles()
    Maps:cartesian()
    self.map = Maps:map(self.tileCountX+1,self.tileCountY+1, Tile,TileEdge, self:asTileFinder())
end

Yes. To make this work we’ll need to know more in Builder than we do now, namely, this intricate bit. And we’ll need a setter in Dungeon. So be it. Let’s try, we’re green and this shouldn’t take long to try.

function DungeonBuilder:createTiles()
    Maps:cartesian()
    local map = Maps:map(self.view:tileCountX()+1,self.view:tileCountY()+1, Tile,TileEdge, self.view:asTileFinder())
    self.view:setMap(map)
end

I think this is the change. I need to change the call:

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

And I need setMap:

function BuilderView:setMap(aMap)
    self.dungeon:setMap(aMap)
end

function Dungeon:setMap(aMap)
    self.map = aMap
end

I expect this to work. It does. Let’s look for other users of createTiles and see if we can do without our method in BuilderView and in Dungeon itself. There seem to be none. Delete the other createRoom methods. Commit: Move createTiles to DungeonBuilder, remove scaffolding.

OK, who’s next?

function DungeonBuilder:setupMonsters()
    local monsters = Monsters()
    monsters:createRandomMonsters(self.RUNNER, self.numberOfMonsters, self.levelNumber, self.playerRoomNumber)
    monsters:createHangoutMonsters(self.RUNNER, 2, self.wayDownTile)
    self.dungeon:setMonsters(monsters)
end

This is righteous, change to refer to view:

function DungeonBuilder:setupMonsters()
    local monsters = Monsters()
    monsters:createRandomMonsters(self.RUNNER, self.numberOfMonsters, self.levelNumber, self.playerRoomNumber)
    monsters:createHangoutMonsters(self.RUNNER, 2, self.wayDownTile)
    self.view:setMonsters(monsters)
end

What’s left? These …

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

function DungeonBuilder:placeWayDown()
    local r1 = self:getRooms()[1]
    local target = r1:centerTile()
    self.wayDownTile = self.dungeon:farawayOpenRoomTile(r1, target)
    local wayDown = WayDown(self.wayDownTile)
    self.RUNNER:setWayDown(wayDown)
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 expect these to be tricky. More important, I’m a bit under the weather today, having had a tooth ripped out yesterday, so I’m going to stop here and sum up.

Summary

First thing: I find that when I’m not physically near my best, I tend to make more mistakes and notice them less. So before doing anything complex, I think it’s best to find someone to work with, or to wait until I’m closer to nominal. YMMV, but I know how I’d bet.

Second, we have seven commits in less than an hour, so that’s pretty decent. We moved a few building methods out of Dungeon and into Builder, which is what we’re here for.

I think the BuilderView is helping a bit, keeping my concerns limited. If we do accidentally try to do anything unauthorized to the Dungeon, we’ll get an error. Perfect.

I’m outa here. When my FTP comes back, I’ll push this article up for my reader.

See you next time, reader!