Working toward the Learning Level. Is it time to start on our ‘Making App’? We do start. Then we revert.

Two articles ago we managed to create a designed dungeon layout of 12 equally-sized rooms, four across and three down. We managed to connect them together in a sort of spiral arrangement, leading down a single path from starting in a corner to a final room in the middle. It was easy enough, but … no, it really wasn’t easy enough. Here’s the code required to set up that dungeon, throw in some monsters and such, and make it navigable:

function GameRunner:createLearningLevel()
    self.dungeonLevel = 1
    TileLock = false
    self:createTiles()
    self:clearLevel()
    self:createLearningRooms()
    self:connectLearningRooms()
    self:convertEdgesToWalls()
    self.monsters = Monsters()
    self:placePlayerInRoom1()
    self:placeWayDown()
    --self:placeSpikes(5)
    --self:placeLever()
    self:setupMonsters(6)
    self.keys = self:createThings(Key,5)
    self:createThings(Chest,5)
    self:createLoots(10)
    self:createDecor(30)
    self:createButtons()
    self.cofloater:runCrawl(self:initialCrawl(self.dungeonLevel))
    self:startTimers()
    self.playerCanMove = true
    TileLock = true
end

You’ll note that the spikes and lever are commented out. The lever is out just because it is useless without spikes. The spikes are out for two reasons. We can rationalize that we don’t want them in a learning level, though in fact we might plant one set somewhere for learning purposes. But the other reason is that spikes go into hallways only, and there are only 11 hallway tiles in our learning level, and I felt it would take ages for the random placement routine to find them.

Having had nearly 48 hours to let the learning settle in my mind, I’ve come to some observations about this exercise:

Separate Dungeon Builder
I’ve been wanting to remove dungeon building from the GameRunner class for ages. GameRunner has at least two large responsibilities: running the game (duh), and building the dungeon. Two responsibilities is one too many for a class. We should break dungeon building off from GameRunner, and perhaps we should break designed dungeons off from whatever builds designed ones.
Random Placement
In most cases, objects are currently placed randomly in the dungeon. This is typically done by a method that is given a number of objects to place, and that then loops, finding a location to put the object, and selecting a random identity for the object, such as a crate or skeleton, or Death Fly or Ghost.

Finding a location is done randomly, probing into the tile space until it finds a suitable tile. Usually that means a tile that has open space all around it. We don’t place things up against walls, which is a hack to ensure that we never accidentally block a hallway.

In our designed levels, we’ll want to say exactly where things go, or perhaps to specify where they go relative to a room’s coordinates. One way or the other, we’ll want to specify the location. This will require some refactoring of our placement functions. Probably what we’ll do is build a new method that produces a list of tiles with a desired property (not in room one and not up against the wall), and then feed that list to the code that places the objects. We might even want to give it a parallel list of the class of object to build, and even another list of the contents of the object if it gives something away.

We’ll discover this when we do it. That I have an idea how it might go is a result of living in this code for 171 articles and in this head for almost 30,000 days.

Room Definition
I started out to build this nice rectangular layout with 12 explicit calls to Room, giving them the right coordinates and such. That quickly got to be more arithmetic than I could perform accurately. The code now looks like this:
function GameRunner:createLearningRooms()
    self.rooms = {}
    local rooms = self.rooms
    local r
    local sx,sy = 2,2
    local w = 11 -- makes room 12 wide
    local h =  7 -- and 8 wide
    local xStep = w +2
    local yStep = h + 2
    for y = 1,3 do
        local yy = sy + yStep*(y-1)
        for x = 1,4 do
            local xx = sx + xStep*(x-1)
            local r = Room(xx,yy, w, h, self, true)
            table.insert(rooms, r)
        end
    end
end
Room Definition (cd)
I think that in general, a dungeon designer will want to draw their dungeon on “graph paper”, laying it out until it expresses whatever design notion they have in mind. Then, somehow, they need to express it to the program. We can probably get them to tell us how wide and tall each room is. And on graph paper, it’s probably not too awful to provide the coordinates of a corner.

If they’re building a very regular arrangement, they might want to provide an x,y separation or something, rather than the coordinates. We’ll leave that for the future, when we have an example. Let’s see if we can refactor the code above to create the rooms explicitly.

I begin by adjusting the width and height to be what we actually want, and get this:

function GameRunner:createLearningRooms()
    self.rooms = {}
    local rooms = self.rooms
    local r
    local sx,sy = 2,2
    local w = 12 -- makes room 12 wide
    local h =  8 -- and 8 high
    local xStep = w + 1
    local yStep = h + 1
    for y = 1,3 do
        local yy = sy + yStep*(y-1)
        for x = 1,4 do
            local xx = sx + xStep*(x-1)
            local r = Room(xx,yy, w-1, h-1, self, true)
            table.insert(rooms, r)
        end
    end
end

: That makes us see the weirdness of the room creation, where we have to subtract 1 from the width and height to get what we want. Why is that?

function Room:init(x,y,w,h, runner, paint)
    if paint == nil then paint = true end
    self.x1 = x
    self.y1 = y
    self.x2 = x + w
    self.y2 = y + h
    self.runner = runner
    if paint then
        self:paint()
    end
end

: The reason, not a very good one, is that the top right corner of the room is set to the bottom left plus (w,h). Which will make it one square wider and higher than called for. That’s just bizarre. We should fix that here and now, right in the middle of our planning.

Digression: Fix The Oddity

We’ll need to change all our room creators to provide the size they actually want, not one less:

function Room:random(spaceX, spaceY, minSize, maxSize, runner)
    local w = math.random(minSize,maxSize)
    local h = math.random(minSize,maxSize)
    local x = math.random(2,spaceX-w - 1) -- leave room for wall
    local y = math.random(2,spaceY-h - 1) -- leave room for wall
    return Room(x,y,w,h,runner, false)
end

This is fine, though I’d like to be more certain about the x and y. Can’t hurt to stay away from the far wall. When we create a room with this, we’re going to get rooms one tile smaller in each dimension than we used to:

function Room:init(x,y,w,h, runner, paint)
    if paint == nil then paint = true end
    self.x1 = x
    self.y1 = y
    self.x2 = x + w - 1
    self.y2 = y + h - 1
    self.runner = runner
    if paint then
        self:paint()
    end
end

So we need to look at our call to random and adjust the parms upward by one.

function GameRunner: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, 4,13, self)
            if self:hasRoomFor(r) then
                placed = true
                table.insert(self.rooms,r)
                r:paint(#self.rooms)
            elseif timeout <= 0 then
                placed = true
            end
        end
    end
end

So, 5,14 up there.

            r = Room:random(self.tileCountX,self.tileCountY, 5,14, self)

We also have this bizarre test:

        _:test("random rooms", function()
            local x1,y1,x2,y2
            for i = 1,100 do
                local r = Room:random(60,60, 4,15, Runner)
                x1,y1,x2,y2 = r:corners()
                _:expect(x1>0 and x1<=60).is(true)
                _:expect(y1>0 and y1<=60).is(true)
                _:expect(x2>0 and x2<=60).is(true)
                _:expect(y2>0 and y2<=60).is(true)
            end
        end)

For now, I’ll just adjust its parms as well. But really?

                local r = Room:random(60,60, 5,16, Runner)

Who else makes Rooms? Lots of tests. I just changed all of them. I’ll spare you the changing of a lot of 20,20 to 21,21. Let’s see if the tests and game run. Yes. Commit: width and height parameters to Room:init are now correct.

Quick Retrospective

What just happened? We were in the middle of exploring the code, planning how to build a designed dungeon, and ran across an oddity in the code. Rather than live with it, rather than make a note about it, we just fixed it. Even with Codea’s rudimentary facilities, the changes just took a few minutes. And now the code is just a bit easier to work with, in an area we’re actually dealing with right now.

Is this prudent? In my view, it’s more than prudent, it’s quite valuable. A small amount of work, and a noticeable improvement in the code.

Now, back to planning. Where were we?

Planning Continues

Room Layout
We have the looping room layout. I would like to write it out longhand, to get a sense of how hard it is to do. Here’s the loop again:
function GameRunner:createLearningRooms()
    self.rooms = {}
    local rooms = self.rooms
    local r
    local sx,sy = 2,2
    local w = 12 -- makes room 12 wide
    local h =  8 -- and 8 high
    local xStep = w + 1
    local yStep = h + 1
    for y = 1,3 do
        local yy = sy + yStep*(y-1)
        for x = 1,4 do
            local xx = sx + xStep*(x-1)
            local r = Room(xx,yy, w-1, h-1, self, true)
            table.insert(rooms, r)
        end
    end
end

This will be a real pain. Let’s just gut it out. I type in this much:

function GameRunner:createLearningRooms()
    self.rooms = {}
    local rooms = self.rooms
    local r
    local sx,sy = 2,2
    local w = 12 -- makes room 12 wide
    local h =  8 -- and 8 high
    local xStep = w + 1 -- 13
    local yStep = h + 1 -- 9
    local r
    r = Room(2,2, w,h)
    r = Room(2,2, w,h)
    r = Room(2,2, w,h)
    r = Room(2,2, w,h)
    r = Room(2,2, w,h)
    r = Room(2,2, w,h)
    r = Room(2,2, w,h)
    r = Room(2,2, w,h)
    r = Room(2,2, w,h)
    r = Room(2,2, w,h)
    r = Room(2,2, w,h)
    r = Room(2,2, w,h)

I see no reason at all to force myself to add numbers like 13 and 9, but let’s try. I’m tempted to change the room size, to be honest. I assume a real dungeon designer would be more concerned about form than numbers, but we can be certain they’re not going to want to be adding a lot of 13s.

I work out reasonably quickly that the x’s probably go 2, 15, 28, 41. I type those in three times. OK, so the y’s go by 9s, 2, 11, 20).

That gets me to here:

    local r
    r = Room(2,2, w,h)
    r = Room(15,2, w,h)
    r = Room(28,2, w,h)
    r = Room(41,2, w,h)
    r = Room(2,11, w,h)
    r = Room(15,11, w,h)
    r = Room(28,11, w,h)
    r = Room(41,11, w,h)
    r = Room(2,20, w,h)
    r = Room(15,20, w,h)
    r = Room(28,20, w,h)
    r = Room(41,20, w,h)

Now I need the other parameters to Room, and to insert them into the table. But wait, what would be easier? How about this:

function GameRunner:createLearningRooms()
    local w = 12
    local h =  8
    local t = {
    {2,2, w,h},
    {15,2, w,h},
    {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},
    }
end

Now we have a table of x,y,w,h. Unnamed but maybe we don’t care. Now a utility function:

function GameRunner:createLearningRooms()
    local w = 12
    local h =  8
    local t = {
        {2,2, w,h},
        {15,2, w,h},
        {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},
    }
    self.rooms = self:makeRoomsFromXYWH(t)
end

function GameRunner:makeRoomsFromXYWH(tab)
    return map(tab, function(e)
        return Room(e[1],e[2],e[3],e[4], self, true)
    end)
end

Remember my map function, that returns the result of applying a function to every element of a table? Makes things like this easy.

We also begin to see how we can make the job of the dungeon designer a bit easier, by letting them provide us just the coordinates of a room. Maybe. We’ll probably want them to give us a textual description of the room.

Shall we start on that? Let’s see. Will they want to describe all the rooms at once, like this, or one at a time? I suspect one at a time is more likely to be useful. So maybe each room is a few lines of text.

What form should the text take? We could go a number of ways, including YAML, JSON, or a tune of our own invention. The latter might be made easier for the designer, at some cost in tooling. JSON is built into Codea. There are YAML libraries available in Lua.

Let’s start in the direction of a tune of our own invention, with the possibility that we’ll push it to YAML or JSON as may strike our fancy.

But first, we should start moving all this over to a new class outside GameRunner. We only have a few methods right now.

Let’s make a new class, DungeonMaker, and move this learning stuff into it.

Warning: Right about here, start skimming. All of this is decent learning, but we’re going to revert most of it out.

I start here:

-- DungeonMaker
-- RJ 20210512

DungeonMaker = class()

function DungeonMaker:init(runner)
    self.runner = runner
end

We’ll clearly need a GameRunner instance to give back all the stuff we build, so that it can run the resulting dungeon.

Let’s leave it to GameRunner, for now, to provide us with a basic instance of Dungeon. We’ll ask for it, I guess:

Looking at Dungeon, I see that it knows the tiles, the GameRunner, and the Player. So let’s just start our class with a Dungeon instance.

function DungeonMaker:init(dungeon)
    self.dungeon = dungeon
end

We should be thinking in terms of some tests for this thing, but we already have code that works. Let’s write a test for it anyway.

function testDungeonMaker()
    CodeaUnit.detailed = false
    
    _:describe("DungeonMaker", function()
        
        _:before(function()
        end)
        
        _:after(function()
        end)
        
        _:test("First Test", function()
            _:expect(2).is(3)
        end)
        
    end)
end

This fails, of course. I have to change the font size in the code that shows the tests when red, there are so many lines of display code. The tests are getting to be a real pain. This needs addressing. Not now, we’re on a mission.

OK. Can we even test this thing? We need a tile array and at least a fake runner for Dungeon to work.

Here’s the before for the tests for Dungeon class:

        _:before(function()
            _bus = Bus
            Bus = EventBus()
            _runner = Runner
            local gm = GameRunner()
            Runner = gm
            gm:createLevel(12)
            dungeon = gm:getDungeon()
            _TileLock = TileLock
            TileLock = false
        end)

Pretty messy but maybe we can do better. Dungeon wants tiles, runner, and player, but I don’t think we’ll use it for that much.

But, you know what? Let’s go another way. Let’s assume that DungeonMaker is going to take over all the work of making a dungeon, including making the tiles and so on. That will let us make smaller test dungeons, which will be a good thing, and maybe in the fullness of time we can improve the test speed by using it.

So new rules, again:

    _:describe("DungeonMaker", function()
        
        _:before(function()
            maker = DungeonMaker(16,12)
        end)
        
        _:after(function()
        end)
        
        _:test("Dungeon has tiles", function()
            local tile = maker:getTile(vec2(16,12))
            _:expect(tile.kind).is(TileEdge)
        end)

That should be enough to get us going.

function DungeonMaker:init(xTiles,yTiles)
    self.tiles = self:makeTiles(xTiles,yTiles)
end

function DungeonMaker:makeTiles(xTiles, yTiles)
    local tiles = {}
    for x = 1,xTiles+1 do
        tiles[x] = {}
        for y = 1,yTiles+1 do
            tiles[x][y] = Tile(x,y,TileEdge, self.runner)
        end
    end
    return tiles
end

So far I’m passing in a nil (self.runner) to the tile for its runner. I’m going to see how long we can do this. In working out how to do all this, I realized that the way dungeon creation happens now is rather intricate:

function GameRunner:createTiles()
    self.tiles = {}
    for x = 1,self.tileCountX+1 do
        self.tiles[x] = {}
        for y = 1,self.tileCountY+1 do
            local tile = Tile:edge(x,y, self)
            self:defineTile(tile)
        end
    end
end

function GameRunner:defineTile(aTile)
    self:getDungeon():defineTile(aTile)
end

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

We’re taking a bit of a risk here. We have a moderately robust way of creating the tiles, collaborating between GameRunner and Dungeon. We’ve sort of been in the mode of moving the function of dungeon creation over to Dungeon. Now we’re moving it to DungeonMaker instead. We’ll need to commit to unwinding this mess unless we want to make the system more difficult to understand than it already is.

Nonetheless, this will create an array of tiles.

1: Dungeon has tiles -- DungeonMaker:19: attempt to call a nil value (method 'getTile')

No surprise there.

function DungeonMaker:getTile(pos)
    return self.tiles[pos.x][pos.y]
end

I expect this to work. Mysteriously, it does. Let’s continue. What’s our point here? Oh, right, to make a learning level.

        _:test("Learning Level", function()
            maker:createLearningLevel()
            local tile = maker:getTile(5,5)
            _:expect(tile.roomNumber).is(1)
        end)

That should be enough to get us going here. I’m just going to move the whole thing over:

function DungeonMaker:createLearningRooms()
    local w = 12
    local h =  8
    local t = {
        {2,2, w,h},
        {15,2, w,h},
        {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},
    }
    self.rooms = self:makeRoomsFromXYWH(t)
end

function DungeonMaker:makeRoomsFromXYWH(tab)
    return map(tab, function(e)
        return Room(e[1],e[2],e[3],e[4], self, true)
    end)
end

function DungeonMaker:connectLearningRooms()
    local dungeon = self:getDungeon()
    local rooms = self.rooms
    rooms[2]:connect(dungeon, rooms[1])
    rooms[3]:connect(dungeon, rooms[2])
    rooms[4]:connect(dungeon, rooms[3])
    rooms[8]:connect(dungeon, rooms[4])
    rooms[12]:connect(dungeon, rooms[8])
    rooms[11]:connect(dungeon, rooms[12])
    rooms[10]:connect(dungeon, rooms[11])
    rooms[9]:connect(dungeon, rooms[10])
    rooms[5]:connect(dungeon, rooms[9])
    rooms[6]:connect(dungeon, rooms[5])
    rooms[7]:connect(dungeon, rooms[6])
end

I’d like to be able to change GameRunner right now to create the level using DungeonMaker but this code isn’t going to work … yet. I’m afraid this bite is too large, so I’ll create tests here to move function into DungeonMaker and then adjust GameRunner’s code when I’m ready. I’m not ready yet. But I do think I want a runner passed in, even though I’m ignoring it, mostly, in the tests.

        _:before(function()
            maker = DungeonMaker(nil, 16,12)
        end)

function DungeonMaker:init(runner, xTiles,yTiles)
    self.runner = runner
    self.tiles = self:makeTiles(xTiles,yTiles)
end

The test doesn’t want to create the learning level yet. Just the rooms:

        _:test("Learning Level", function()
            maker = DungeonMaker(nil, 85,64)
            maker:createLearningRooms()
            local tile = maker:getTile(5,5)
            _:expect(tile.roomNumber).is(1)
        end)

Let’s see how this explodes. Moving that much code over surely won’t just work, will it?

2: Learning Level -- Tile:76: Invariant Failed: Tile must receive Runner

Hm, I’m too clever for my shirt.

function Tile:init(x,y,kind, runner, roomNumber)
    if runner ~= Runner then
        print(runner)
        assert(false, "Invariant Failed: Tile must receive Runner")
    end
    self.position = vec2(x,y)
    self.kind = kind
    self.runner = runner
    self.roomNumber = roomNumber
    self:initDetails()
end

That’s an interesting check. And a fatal crash. Let’s do a fake runner for now.

        _:before(function()
            savedRunner = Runner
            Runner = FakeRunner()
            maker = DungeonMaker(Runner, 16,12)
        end)
        
        _:after(function()
            Runner = savedRunner
        end)

Setting up this test is getting a bit tedious. But I can tell, looking at the current code, that making it work without tests will involve more debugging than we should do, and once we get the basic test frame in place, we’ll be in good shape.

Curiously, with this change in place, I still get the error. This is mostly evidence that the Runner / Room / Tile nest is too complicated. But I do wonder why this failed.

        _:test("Learning Level", function()
            maker = DungeonMaker(Runner, 85,64)
            maker:createLearningRooms()
            local tile = maker:getTile(5,5)
            _:expect(tile.roomNumber).is(1)
        end)

Looking more carefully at how tiles are created, I do this:

function DungeonMaker:makeTiles(xTiles, yTiles)
    local tiles = {}
    for x = 1,xTiles+1 do
        tiles[x] = {}
        for y = 1,yTiles+1 do
            tiles[x][y] = Tile:edge(x,y,self.runner)
        end
    end
    return tiles
end

Full Disclosure

My test above first started giving me an error about the wrong runner, then started an infinite loop in Codea, which I had to crash out of. I’ve been wasting time shaving that yak, but I’ve “isolated” the problem to the call to createLearningRooms. Let’s review that:

function DungeonMaker:createLearningRooms()
    local w = 12
    local h =  8
    local t = {
        {2,2, w,h},
        {15,2, w,h},
        {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},
    }
    self.rooms = self:makeRoomsFromXYWH(t)
end

function DungeonMaker:makeRoomsFromXYWH(tab)
    return map(tab, function(e)
        return Room(e[1],e[2],e[3],e[4], self, true)
    end)
end

Ah. Would that I had looked at that sooner. We’re passing in self as the runner, and we’re not a runner.

function DungeonMaker:makeRoomsFromXYWH(tab)
    return map(tab, function(e)
        return Room(e[1],e[2],e[3],e[4], self.runner, true)
    end)
end

Let’s see if our test will run now. And indeed it does.

I think I’ll commit this: initial DungeonMaker. It’s mostly harmless, although I do have the game in LearningMode at the moment.

Let’s look back at how the GameRunner builds the learning level, to see what to test next.

function GameRunner:createLearningLevel()
    self.dungeonLevel = 1
    TileLock = false
    self:createTiles()
    self:clearLevel()
    self:createLearningRooms()
    self:connectLearningRooms()
    self:convertEdgesToWalls()
    self.monsters = Monsters()
...

Yes. Time to make the connecting work. And perhaps we should make the current test a bit more robust, like check a second room.

Oh my, I didn’t even really make it run yet, as I had stuff commented out. Brilliant. Sounds like time for a break, but not yet.

2: Learning Level -- Room:84: attempt to call a nil value (method 'defineTile')

Arrgh. This is why I don’t already have the GameRunner / Tile stuff untangled. But who even calls Room:defineTile?

Ah. It’s in Room:paint:

function Room:paint(roomNumber)
    for x = self.x1,self.x2 do
        for y = self.y1,self.y2 do
            self.runner:defineTile(self:correctTile(x,y, roomNumber))
        end
    end
end

Let’s not paint these rooms, at least not yet.

function DungeonMaker:makeRoomsFromXYWH(tab)
    return map(tab, function(e)
        return Room(e[1],e[2],e[3],e[4], self.runner, false)
    end)
end
2: Learning Level -- DungeonMaker:47: attempt to index a number value (local 'pos')

Test fails to say vec2:

        _:test("Learning Level", function()
            DungeonMakerTest = true
            maker = DungeonMaker(Runner, 85,64)
            maker:createLearningRooms()
            local tile = maker:getTile(vec2(5,5))
            _:expect(tile.roomNumber).is(1)
            DungeonMakerTest = false
        end)
2: Learning Level  -- Actual: nil, Expected: 1

I think we’re not passing in the room number to the room creation.

I need to modify map:

function map(tab,f)
    local r = {}
    for k,v in ipairs(tab) do
        r[k] = f(v,k)
    end
    return r
end

I passed in the k as a second parameter, That’s the table index. Ideally, I’d call k,v but not just now. Now I can use it:

function DungeonMaker:makeRoomsFromXYWH(tab)
    return map(tab, function(e, roomNumber)
        return Room(e[1],e[2],e[3],e[4], self.runner, false, roomNumber)
    end)
end

This may do the job, but I’m not sure where room number gets set. Let’s check Room.

OK, this is odd. Rooms do paint themselves, unless we tell them explicitly not to. But GameRunner calls paint explicitly, with the room number. I can add it to the room’s main init safely, and do this:

Meh. This is a bit too entangled to lead to a good end.

I think I’ll paint the directly, as does GameRunner.

function DungeonMaker:createLearningRooms()
    local w = 12
    local h =  8
    local t = {
        {2,2, w,h},
        {15,2, w,h},
        {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},
    }
    self.rooms = self:makeRoomsFromXYWH(t)
    self:paintRooms()
end

function DungeonMaker:paintRooms()
    map(self.rooms, function(room, number) 
        room:paint(number)
    end)
end

If this works quickly, we’ll wrap up. If not, we’l revert and wrap up.

2: Learning Level -- Room:84: attempt to call a nil value (method 'defineTile')

Right. Been there, done that. Let’s put a defineTile onto our FakeRunner:

function FakeRunner:defineTile(tile) 
end

That’s insufficient. We’ll need to do it more or less right:

Paint does this:

function Room:paint(roomNumber)
    for x = self.x1,self.x2 do
        for y = self.y1,self.y2 do
            self.runner:defineTile(self:correctTile(x,y, roomNumber))
        end
    end
end

It actually creates a room tile and puts it into the array. This is so that tiles can be immutable, I guess, or else it’s just weird.

My fake object is making this too hard, and the code relies too strongly on GameRunner, which we already know.

Popping Head Up from Deep in the Muck

I think what needs to happen before we go much further with the designed levels, is to untangle the GameRunner / Dungeon / Tile connections a bit. If we were a team of more than one, one pair might work on the text language side of the dungeon design issue while another pair works on the improvement to the tangle, but we only have me, and I can really only do one thing at a time.

Now, there is another way … and we owe it to those who pay us to at least consider it.

The point of the DungeonMaker object we worked on today is to offload dungeon creation, out of the GameRunner, to somewhere else. Almost anywhere else. It’s beginning to look like the current scheme is too big a bite, and that it’ll be just one thing after another to make it work.

What do we do when a bite is too big, class?

We revert and take a smaller bite! – The class

Right. That’s what we do. So let’s ditch this code, which isn’t all that lovely anyway, and come back at it next time with a more incremental approach.

We’ll decide that approach with fresh eyes tomorrow, but today I think what we’ll do is to create a more utility-oriented class that can make arrays of tiles and such. Maybe that will be a new class, maybe we’ll put the stuff on Dungeon. It’ll require a review of Dungeon to decide that.

What will happen, I believe, is that we’ll get some code offloaded out of GameMaker, but that it’ll probably still not feel like it’s in quite the right place.

And possibly, some of the ideas we have right now are not in the right place. The tile, for example, knows its room number. Maybe that should be represented another way entirely. It’s not obviously a dumb idea.

We’ll find out in due time. Today, we’ll revert the code and keep the contents of our head.

See you next time!