I feel the need to make Dung a bit more like a game, with levels and such. Let’s start pushing in that direction.

It’s a small amount of fun driving the princess around, whacking monsters and picking up gems, but there’s no real sense of progress. You can’t even get to a new part of the dungeon: once you’ve exhausted all the rooms of the level, there’s nothing else to do.

There needs to be a kind of “Hero’s Journey” to a decent game, and while I’m no game designer, I can at least do my best to put in facilities that would let a good game designer specify and plug in interesting bits. Let’s think here about some “interesting bits” that we probably would like to provide for:

Levels
The game certainly needs more than one maze with a dozen rooms in it. When you’ve cleared one level, there should be some way to get to the next, even more terrifying level.
Leveling Up
Deeper levels in the dungeon should be harder, and the player therefore needs some say to “level up”, to gain in power and capability, so as to have some way of coping with increased difficulty.
Increased Difficulty
Lower levels should have new and tougher monsters. They should be more difficult in other ways. I’m not sure just what those ways might be. Perhaps puzzles.
Puzzles
There could be puzzles that the player has to solve. Perhaps there are touchstones that have to be touched in a certain order, or locations into which certain items have to be placed. Solving a puzzle could provide new treasures, or could be used simply to open the door into another level. Some puzzles might kill.
Monsters
I think that different, tougher monsters should appear at the levels go down. Perhaps some puzzles, solved incorrectly, or traps stepped upon, should trigger a monster attack.
Randomness
Presently the game just strews monsters and treasures around at random. You can find yourself rezzing into a room with three chests, two gems, and a deadly pear-eating partridge. I think we should change that part of setup, probably rezzing treasures and monsters in rooms when the player arrives, so that we can have a bit more control over what she encounters, and when.
Combat
There need to be weapons, armor, continued healing, poisons, magic, and I believe, no death. Maybe if your health goes to zero, you lose some or all of your valuables and wake up in an unknown part of the dungeon. Maybe you go back a level. Something.
Repeatable levels
We could, in principle, save the random seed when we create a level, so that we could create the very same map again. I’m not sure what the value to that would be in general, but I can certainly see the advantage to having some levels with more designed shapes with special purposes. Those probably wouldn’t be computed with random numbers, however.
Boss Rooms
Because of the random dungeon creation we use, there is no telling what the dungeon will look like. It could, in principle, turn out to be one large irregularly-shaped room, if the dice rolled just right. So as things stand, I don’t see a good way to select a boss room from the random map. I have some ideas, and we can try some experiments. One way or another, a difficult room before you can progress is a staple of most games and we should have it.
Decor
We have a few variable tiles, but no other interesting decor. I’ve held back a few tiles, a button one, and one with pop-up spikes. And I’ve found some decor items, ransom skulls and bones that one could leave about, and so on. We could spice things up with a bit of that.
Darkness
The game’s tiles are quite dark. You may recall that I used to have a fade-to-dark effect around the player, and when I went to these new tiles, I didn’t like the effect. But there could be value to some kind of darkness effect.

Wow. When you look at all the things that games do, that we have not yet implemented, you begin to see why real games use many programmers and designers and artists, and why they so often do not release on time. I’m not sure I want to run this series of articles out to Dungeon 996. I’m not even sure it’s worth going to 103, because I have no idea whether many folks are out there reading.

I’m just here to have fun, anyway. So …

What’s Next?

Let’s start on levels. We probably want some of these things:

  • A special entry to the next level, a door or stair;
  • Monsters differ between levels, tougher as we go;
  • Treasures differ, better the lower you go

I also think, that as part of this push toward being more interesting, I’d like to change how the monsters and treasures are created. Rather than placing them entirely randomly when the dungeon is created, maybe we could do something like make the decision when an area is entered, what is in it. Maybe there are even actions the player can take that makes things happen.

Let’s start with something new, that might be easy. We’ll devise a tile that looks like steps downward, and if you step on it, you find yourself in a new level. Once we make that work, we’ll work on populating the levels differently.

Having thought about it for a few seconds, I have an idea for how it could work. Tiles have contents, such as Loots and Chests. We could give our stairway tile a special contents item, NewLevel. Then we’ll just generate a new level and plunk the princess down in it.1

So let’s start on …

A Way Down

Our create level code looks like this:

function GameRunner:createLevel(count)
    self:createRandomRooms(count)
    self:connectRooms()
    self:convertEdgesToWalls()
    local r1 = self.rooms[1]
    local rcx,rcy = r1:center()
    local tile = self:getTile(vec2(rcx,rcy))
    self.player = Player(tile,self)
    self.monsters = self:createThings(Monster,9)
    for i,monster in ipairs(self.monsters) do
        monster:startAllTimers()
    end
    self.keys = self:createThings(Key,5)
    self:createThings(Chest,5)
    self:createLoots(10)
    self.buttons = {}
    table.insert(self.buttons, Button("left",100,200, 64,64, asset.builtin.UI.Blue_Slider_Left))
    table.insert(self.buttons, Button("up",200,250, 64,64, asset.builtin.UI.Blue_Slider_Up))
    table.insert(self.buttons, Button("right",300,200, 64,64, asset.builtin.UI.Blue_Slider_Right))
    table.insert(self.buttons, Button("down",200,150, 64,64, asset.builtin.UI.Blue_Slider_Down))
    self:createCombatButtons(self.buttons)
    self.cofloater:runCrawl(self:initialCrawl())
    self.playerCanMove = true
end

That could certainly use a bit of refactoring, and since we’re here now, let’s at least do this:

function GameRunner:createLevel(count)
    self:createRandomRooms(count)
    self:connectRooms()
    self:convertEdgesToWalls()
    local r1 = self.rooms[1]
    local rcx,rcy = r1:center()
    local tile = self:getTile(vec2(rcx,rcy))
    self.player = Player(tile,self)
    self.monsters = self:createThings(Monster,9)
    for i,monster in ipairs(self.monsters) do
        monster:startAllTimers()
    end
    self.keys = self:createThings(Key,5)
    self:createThings(Chest,5)
    self:createLoots(10)
    self:createButtons()
    self.cofloater:runCrawl(self:initialCrawl())
    self.playerCanMove = true
end

function GameRunner:createButtons()
    self.buttons = {}
    table.insert(self.buttons, Button("left",100,200, 64,64, asset.builtin.UI.Blue_Slider_Left))
    table.insert(self.buttons, Button("up",200,250, 64,64, asset.builtin.UI.Blue_Slider_Up))
    table.insert(self.buttons, Button("right",300,200, 64,64, asset.builtin.UI.Blue_Slider_Right))
    table.insert(self.buttons, Button("down",200,150, 64,64, asset.builtin.UI.Blue_Slider_Down))
    self:createCombatButtons(self.buttons)
end

OK, a tiny bit better. We should do the monsters etc, but I think all that is going to change soon. We’re here to create a way down to a new level. Let’s begin by making it work, and then we’ll think about placement.

This patch of code has meaning:

    local r1 = self.rooms[1]
    local rcx,rcy = r1:center()
    local tile = self:getTile(vec2(rcx,rcy))
    self.player = Player(tile,self)

Let’s extract it while we’re here. Yes, we’re trying to build a way down, but we need a clean workspace.

function GameRunner:placePlayerInRoom1()
    local r1 = self.rooms[1]
    local rcx,rcy = r1:center()
    local tile = self:getTile(vec2(rcx,rcy))
    self.player = Player(tile,self)
end

With the call where the code used to be. Now, I think I’d like to put the WayDown into room 1 for testing purposes. So let’s do this …

function GameRunner:createLevel(count)
    self:createRandomRooms(count)
    self:connectRooms()
    self:convertEdgesToWalls()
    self:placePlayerInRoom1()
    self:placeWayDown() -- <---
    self.monsters = self:createThings(Monster,9)
    for i,monster in ipairs(self.monsters) do
        monster:startAllTimers()
    end
    self.keys = self:createThings(Key,5)
    self:createThings(Chest,5)
    self:createLoots(10)
    self:createButtons()
    self.cofloater:runCrawl(self:initialCrawl())
    self.playerCanMove = true
end

Right, solved. Well, not quite.

function GameRunner:placeWayDown()
    local r1 = self.rooms[1]
    local rcx,rcy = r1:center()
    local tile = self:getTile(vec2(rcx-2,rcy-2))
    WayDown(tile,self)
end

I’m placing it down and to the left of the player. I’m naming a new class WayDown, and I expect it to work like Keys and Chests and such do. I’ll create the class.

--WayDown
-- RJ 20210222

WayDown = class()

function WayDown:init(tile,runner)
    self.runner = runner
    self.tile = tile
    self.tile:addDeferredContents(self)
end

function WayDown:draw()
    
end

Now I need to see about drawing this thing. Tile draw their contents: I just looked to be sure. So I think I can “just” draw my stairs image right over whatever the floor tile looks like.

Let’s start with a rectangle.

function WayDown:draw()
    pushMatrix()
    pushStyle()
    rectMode(CENTER)
    local c = self.tile:graphicCenter()
    fill(255,0,0)
    rect(0,0,60,60)
    popStyle()
    popMatrix()
end

This might actually work. Curiously, it doesn’t. Let’s look at a Loot and see how they do it. Hm they do the same thing but with a Sprite. Oh, I see, I didn’t translate to the location, or use it.

function WayDown:draw()
    pushMatrix()
    pushStyle()
    rectMode(CENTER)
    local c = self.tile:graphicCenter()
    fill(255,0,0)
    rect(c.x,c.y,60,60)
    popStyle()
    popMatrix()
end

That gives me what I was looking for:

red

So that’s good. Let’s see about interacting.

Interaction with contents is managed by the TileArbiter class, which contains a table of operations:

function TileArbiter:createTable()
    -- table is [resident][mover]
    if TA_table then return end
    local t = {}
    t[Chest] = {}
    t[Chest][Monster] = {moveTo=TileArbiter.refuseMove}
    t[Chest][Player] = {moveTo=TileArbiter.refuseMove, action=Player.startActionWithChest}
    t[Key] = {}
    t[Key][Monster] = {moveTo=TileArbiter.acceptMove}
    t[Key][Player] = {moveTo=TileArbiter.acceptMove, action=Player.startActionWithKey}
    t[Player] = {}
    t[Player][Monster] = {moveTo=TileArbiter.refuseMove, action=Monster.startActionWithPlayer}
    t[Monster]={}
    t[Monster][Monster] = {moveTo=TileArbiter.refuseMoveIfResidentAlive, action=Monster.startActionWithMonster}
    t[Monster][Player] = {moveTo=TileArbiter.refuseMoveIfResidentAlive, action=Player.startActionWithMonster}
    t[Loot] = {}
    t[Loot][Player] = {moveTo=TileArbiter.refuseMove, action=Player.startActionWithLoot}
    t[Loot][FakePlayer] = {moveTo=TileArbiter.refuseMove, action=FakePlayer.startActionWithLoot}
    TA_table = t
end

For each class of item, we make table entries for monster and player. If we leave one or the other out, I think the move is accepted. For the WayDown, we don’t care what the move does, because we’re going to beam her out, and the action should be startActionWithWayOut, because all actions are sent to the Entity entering the tile.

    t[WayOut] = {}
    t[WayOut][Player] = {moveTo=TileArbiter.acceptMove, action=Player.startActionWithWayOut}

This should give me a failure to find startActionWithWayOut. What I actually get is this:

TileArbiter:62: table index is nil
stack traceback:
	TileArbiter:62: in method 'createTable'
	TileArbiter:13: in field 'init'
	... false
    end

    setmetatable(c, mt)
    return c
end:24: in global 'TileArbiter'
	Tile:81: in method 'attemptedEntranceBy'
	Tile:306: in function <Tile:304>
	(...tail calls...)
	Monster:184: in method 'makeRandomMove'
	Monster:120: in method 'chooseMove'
	GameRunner:248: in method 'moveMonsters'
	GameRunner:282: in method 'playerTurnComplete'
	Player:196: in method 'turnComplete'
	Player:138: in method 'keyPress'
	GameRunner:238: in method 'keyPress'
	Main:34: in function 'keyboard'

After some confusion, I realize that this is saying that it can’t execute this:

    t[WayOut] = {}

That tells me that the definition of WayOut must not have been picked up. Which finally sinks into my brain, the class name is WayDown. Sometimes I amaze myself.

OK, I get an error now when I step in, but not the one I expect:

TileArbiter:17: attempt to call a nil value (method 'getTile')
stack traceback:
	TileArbiter:17: in field 'moveTo'
	TileArbiter:28: in method 'moveTo'
	Tile:82: in method 'attemptedEntranceBy'
	Tile:306: in function <Tile:304>
	(...tail calls...)
	Player:143: in method 'moveBy'
	Player:99: in method 'executeKey'
	Player:137: in method 'keyPress'
	GameRunner:238: in method 'keyPress'
	Main:34: in function 'keyboard'

We need this, part of the standard protocol for a tile contents item:

function WayDown:getTile()
    return self.tile
end

I am surprised not to get the expected error. Ah, this:

function TileArbiter:moveTo()
    local entry = self:tableEntry(self.resident,self.mover)
    local action = entry.action
    if action then action(self.mover,self.resident) end
    local result = entry.moveTo(self)
    return result
end

If the Entity doesn’t support the action, it’s not sent. Fine. We want the Player to do this:

function Player:startActionWithWayDown(aWayDown)
    aWayDown:actionWith(self)
end

I was thinking to trigger the rolling of a new level in Player, but generally we call back to the other guy, and we may want other kinds of action in the WayDown anyway, such as checking whether the player is “ready”. For now, however:

function WayDown:actionWith(aPlayer)
    self.runner:createLevel()
end

I get this error. I knew it couldn’t be this easy.

GameRunner:105: attempt to compare number with nil
stack traceback:
	GameRunner:105: in method 'createRandomRooms'
	GameRunner:69: in method 'createLevel'
	WayDown:13: in method 'actionWith'
	Player:196: in local 'action'
	TileArbiter:27: in method 'moveTo'
	Tile:82: in method 'attemptedEntranceBy'
	Tile:306: in function <Tile:304>
	(...tail calls...)
	Player:143: in method 'moveBy'
	Player:99: in method 'executeKey'
	Player:137: in method 'keyPress'
	GameRunner:238: in method 'keyPress'
	Main:34: in function 'keyboard'

The createLevel wants to know how many rooms to create.

function WayDown:actionWith(aPlayer)
    self.runner:createLevel(12)
end
Tile:14: Attempt to create room tile when locked
stack traceback:
	[C]: in function 'assert'
	Tile:14: in function <Tile:13>
	(...tail calls...)
	Room:80: in method 'paint'
	GameRunner:114: in method 'createRandomRooms'
	GameRunner:69: in method 'createLevel'
	WayDown:13: in method 'actionWith'
	Player:196: in local 'action'
	TileArbiter:27: in method 'moveTo'
	Tile:82: in method 'attemptedEntranceBy'
	Tile:306: in function <Tile:304>
	(...tail calls...)
	Player:143: in method 'moveBy'
	Player:99: in method 'executeKey'
	Player:137: in method 'keyPress'
	GameRunner:238: in method 'keyPress'
	Main:34: in function 'keyboard'

It’s not that easy to just create a new level. We’ve locked the tiles as a precaution. So … what does it take? We need to read some code here, and probably improve it.

The original level is created by main, this way:

    TileLock = false
    Runner = GameRunner()
    Runner:createLevel(12)
    TileLock = true

So nothing can be required other than whatever GameRunner:init does:

function GameRunner:init()
    self.tileSize = 64
    self.tileCountX = 85 -- if these change, zoomed-out scale 
    self.tileCountY = 64 -- may also need to be changed.
    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:setTile(tile)
        end
    end
    self.cofloater = Floater(self, 50,25,4)
    self.musicPlayer = MonsterPlayer(self)
end

We really ought to create the tiles in createLevel.

Time to think a bit here.

Time to Think

It’s 1010, and I’ve been working since 0835, and while just cracking things in has worked so far, we’ve just hit a spot where the system’s design doesn’t quite support what we need to do. The right thing for me to do, based on my experience, is to take a break, and then come back to it. Just a short break, long enough to make a chai latte and come back to the desk.

I’m back after a brief tech support call to my wife’s iMac. Not quite the break one wanted, but I did have time to make a chai.

The TileLock (global) prevents us from creating new tiles after the game has created a level. I think we do need to at least set all the tiles back to edge. I’m inclined to clear to edge in createLevel, and to do the locking there.

function GameRunner:createLevel(count)
    TileLock=false
    self:clearLevel()
    self:createRandomRooms(count)
    self:connectRooms()
    self:convertEdgesToWalls()
    self:placePlayerInRoom1()
    self:placeWayDown()
    self.monsters = self:createThings(Monster,9)
    for i,monster in ipairs(self.monsters) do
        monster:startAllTimers()
    end
    self.keys = self:createThings(Key,5)
    self:createThings(Chest,5)
    self:createLoots(10)
    self:createButtons()
    self.cofloater:runCrawl(self:initialCrawl())
    self.playerCanMove = true
    TileLock = true
end

function GameRunner:clearLevel()
    for i,row in ipairs(self.tiles) do
        for j,tile in ipairs(row) do
            tile:convertToEdge()
        end
    end
end

function Tile:convertToEdge()
    self.kind = TileEdge
end

This might suffice, if I’m lucky. If it doesn’t, we may have to recreate the tiles. Oh … we had better at least ditch all their contents, hadn’t we? Back to Tile for a closer look:

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

function Tile:init(x,y,kind, runner)
    self.position = vec2(x,y)
    self.kind = kind
    self.runner = runner
    self.contents = DeferredTable()
    self.seen = false
    self:clearVisible()
    self.tile = nil
end

For now, I’ll copy-paste, while hating myself.

function Tile:convertToEdge()
    self.kind = TileEdge
    self.contents = DeferredTable()
    self.seen = false
    self:clearVisible()
    self.tile = nil
end

We’ll sort out the duplication when this works. I’m getting a tiny bit concerned but am optimistic that this will fly.

levels

OK, super. We seem to be ok. Our WayDown contents item creates a new level and puts us in it. Commit: WayDown tile creates new level.

Now let’s clean this up a bit, extracting the common stuff between Tile:init and Tile:convertToEdge:

function Tile:init(x,y,kind, runner)
    self.position = vec2(x,y)
    self.kind = kind
    self.runner = runner
    self:initDetails()
end

function Tile:initDetails()
    self.contents = DeferredTable()
    self.seen = false
    self:clearVisible()
    self.tile = nil
end

function Tile:convertToEdge()
    self.kind = TileEdge
    self:initDetails()
end

Now let’s draw a stairway. I think I’m OK with drawing it as the contents, though that is admittedly a bit of a hack.

This is going to require some retraining of the programmer. I have to figure out, once again, how to get new tiles into the game. I’m sure that I have some in the Files app.

I find the picture I want, “Steps_Down” in Files, then long-press it to get the popup menu, select Move, which means “Copy”, and select the Dropbox.Assets folder under Codea.

Now Codea should be able to find it. Opening its asset finder, I find the file in Codea’s “Dropbox”, and select it, tap Edit, tap AddTo and after only a few more moves, the picture is in the D2 project where it belongs.

I add it to the drawing code:

function WayDown:draw()
    pushMatrix()
    pushStyle()
    spriteMode(CENTER)
    local c = self.tile:graphicCenter()
    sprite(asset.steps_down,c.x,c.y,64,64)
    popStyle()
    popMatrix()
end

Let’s see how it looks:

steps

That’s nearly good. Since this tile is from the same tile set as the flooring, it matches nicely. Commit: WayDown tile shows steps downward.

Now What?

I think I’d like to move toward the dungeon items being better placed. Items are placed using this function:

function GameRunner:randomRoomTile()
    while true do
        local pos = vec2(math.random(1, self.tileCountX), math.random(1,self.tileCountY))
        local tile = self:getTile(pos)
        if tile:isOpenRoom() then return tile end
    end
end

function Tile:isOpenRoom()
    local r
    for x = -1,1 do
        for y = -1,1 do
            r = self:getNeighbor(vec2(x,y))
            if r.kind ~= TileRoom then return false end
        end
    end
    return true
end

This code is just looking for a tile of type TileRoom that isn’t against a wall. This ensures that the object can’t block a door. I think I’d like to also ensure that there isn’t anything else in the tile yet. But we’ll see where we go with this.

The main point is that this code isn’t placing things in rooms, it’s placing things on room tiles with space around them.

Maybe this is OK, with one exception. Mostly I just don’t like having stuff showing up in the starting room. So what if we make this function know just enough about rooms to avoid room 1, which is where we always place the player.

I wonder whether tiles should know their room number. That might be interesting and useful. Here’s how we create random rooms:

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
                r:paint()
                table.insert(self.rooms,r)
            elseif timeout <= 0 then
                placed = true
            end
        end
    end
end

function GameRunner:hasRoomFor(aRoom)
    return aRoom:isAllowed(self)
end

function Room:isAllowed()
    for x = self.x1,self.x2 do
        for y = self.y1,self.y2 do
            local t = self.runner:getTile(vec2(x,y))
            if not t:isEdge() then return false end
        end
    end
    return true
end

This ensures that all the tiles in the room’s rectangle are edges, which at this point means they are virgo intacta never before used. Rooms cannot overlap (though they can wind up adjacent).

That’s all good. Let’s see what we do with paint:

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

function Room:correctTile(x,y)
    if false and (x == self.x1 or x == self.x2 or y == self.y1 or y == self.y2) then
        return Tile:wall(x,y, self.runner)
    else
        return Tile:room(x,y, self.runner)
    end
end

We create a room tile in the space except around the perimeter, where we create wall tiles.

I see no problem with telling each tile its room number if we cared to, and then we could have our object placement code check for rooms to avoid.

Let’s try it. This code is nearly good for my purpose:

            if self:hasRoomFor(r) then
                placed = true
                r:paint()
                table.insert(self.rooms,r)

I’d prefer that we put the room away before painting because then the table size is the current room number, so:

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

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

function Room:correctTile(x,y, roomNumber)
    if false and (x == self.x1 or x == self.x2 or y == self.y1 or y == self.y2) then
        return Tile:wall(x,y, self.runner, roomNumber)
    else
        return Tile:room(x,y, self.runner, roomNumber)
    end
end

function Tile:room(x,y, runner, roomNumber)
    assert(not TileLock, "Attempt to create room tile when locked")
    return Tile(x,y,TileRoom, runner, roomNumber)
end

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

function Tile:init(x,y,kind, runner, roomNumber)
    self.position = vec2(x,y)
    self.kind = kind
    self.runner = runner
    self.roomNumber = roomNumber
    self:initDetails()
end

I think this puts roomNumber in all the pure room tiles, not the walls or edges. We’re good with that. Now we can use the room number when we place things.

function GameRunner:randomRoomTile()
    while true do
        local pos = vec2(math.random(1, self.tileCountX), math.random(1,self.tileCountY))
        local tile = self:getTile(pos)
        if tile:isOpenRoom() then return tile end
    end
end

We can enhance that this way:

function GameRunner:randomRoomTile(roomToAvoid)
    while true do
        local pos = vec2(math.random(1, self.tileCountX), math.random(1,self.tileCountY))
        local tile = self:getTile(pos)
        if tile:getRoomNumber() ~= roomToAvoid and tile:isOpenRoom() then return tile end
    end
end

function Tile:getRoomNumber()
    return self.roomNumber
end

I suppose this will blow up: there was a lot of drilling down and up here, but if it works it should keep the first room empty, except for WayDown, which will be there for now.

Well, it would if I used the parameter:

function GameRunner:createThings(aClass, n)
    local things = {}
    for i = 1,n or 1 do
        local tile = self:randomRoomTile(1)
        table.insert(things, aClass(tile,self))
    end
    return things
end

function GameRunner:createLoots(n)
    local loots = {}
    for i =  1, n or 1 do
        local tile = self:randomRoomTile(1)
        local tab = RandomLootInfo[math.random(1,#RandomLootInfo)]
        table.insert(loots, Loot(tile, tab[1], tab[2], tab[3]))
    end
    return loots
end

Sure enough, the first room starts out empty, as intended.

Well, almost. Once in a rare while, I’ve seen something appear in the first room. I’m pretty sure that I know what’s happening. Things are placed after the rooms are connected. When rooms are connected, we draw a line of tiles from center of one to center of the other, using straight horizontal and vertical paths, and we set those tiles to type room:

function GameRunner:connectRooms()
    for i,r in ipairs(self.rooms) do
        if i > 1 then
            r:connect(self.rooms[i-1])
        end
    end
end

function Room:connect(aRoom)
    if math.random(1,2) == 2 then
        self:hvCorridor(aRoom)
    else
        self:vhCorridor(aRoom)
    end
end

function Room:hvCorridor(aRoom)
    local startX,startY = self:center()
    local endX,endY = aRoom:center()
    self.runner:horizontalCorridor(startX,endX,startY)
    self.runner:verticalCorridor(startY,endY,endX)
end

function GameRunner:horizontalCorridor(fromX, toX, y)
    fromX,toX = math.min(fromX,toX), math.max(fromX,toX)
    for x = fromX,toX do
        self:setTile(Tile:room(x,y, self))
    end
end

And so on. So we have set some of the room tiles, the ones from its walls to its center, not to have the same (or any) room number.

The easy fix, I think, is to create all the stuff before we connect the rooms.

function GameRunner:createLevel(count)
    TileLock=false
    self:clearLevel()
    self:createRandomRooms(count)
    self:placePlayerInRoom1()
    self:placeWayDown()
    self.monsters = self:createThings(Monster,6)
    for i,monster in ipairs(self.monsters) do
        monster:startAllTimers()
    end
    self.keys = self:createThings(Key,5)
    self:createThings(Chest,5)
    self:createLoots(10)
    self:createButtons()
    self:connectRooms()
    self:convertEdgesToWalls() -- after placement of things
    self.cofloater:runCrawl(self:initialCrawl())
    self.playerCanMove = true
    TileLock = true
end

Another alternative would be to have the new hallway tiles take on the room number of any room that they cross. That might be a better idea. Let’s see if this works, first.

no

I think it’s safe to say, no, that doesn’t work. But it’s more weird than you think. As soon as I move the player, the screen updates and all the tiles paint correctly.

paint

I really wish I had committed a bit ago. Let me back out this last change, moving the hall creation.

Now I think I can safely commit: “things /usually/ don’t show up in the starting room. More to do.”

It is not 1155, which is past my usual quitting time for these articles. Let’s sum up.

Summary

We’ve added a very nice new capability, the WayDown, a tile contents item that rolls up a new dungeon and puts you in it. This will be a good token to move between levels. And I’m pleased at how nicely the basic capability went in. Create a new kind of tile contents; make it draw in a rudimentary fashion; wire it up to take action; give it better graphics. That part went very smoothly.

There are issues: I’ve seen the odd graphics effect that we saw there in the last pictures, in a new dungeon before the placement changes. It happens just once in a while, and I suspect it has to do with visibility.

Looking at it more abstractly, we have an issue in our design, which is that the GameRunner was never asked to create a new dungeon on the fly. We always do it with a full reset. So even though I took care to do things right, it seems that there are some tag ends sticking out and causing trouble.

This often happens. We’re working incrementally, so we expect that today’s code may not be sufficient for tomorrow. That’s why we try to have code that is tested enough, and well-structured enough, to support change. We’re in the business of changing code, as GeePaw Hill puts it, not in the business of writing perfect lasts-forever code.

What we see here is that the game’s design isn’t robust enough, around the GameRunner-Room-Tile axes. At this moment, I’m not sure why what’s here now sometimes seems to display extra tiles. It could even be some kind of timing error as we draw and clear tiles at the same time. Certainly some things are happening in a different order from game init.

More clear is the fact that we have multiple concerns going on in GameRunner. It’s a dungeon creator as well as a game runner. It’s a central communications point for all the objects. It’s got a lot going on. As we change our way from where we are to our next level, we had better start separating out those concerns, if only to help ourselves understand what matters and what doesn’t.

And in thinking about it here, I just realized we have an interesting defect:

function GameRunner:placePlayerInRoom1()
    local r1 = self.rooms[1]
    local rcx,rcy = r1:center()
    local tile = self:getTile(vec2(rcx,rcy))
    self.player = Player(tile,self)
end

This code, called from createLevel, creates a new Player instance. That means she gets all new health and other points, and loses any extra items that she has already accumulated.

Again, evidence of a design that can’t bear the weight of our new requirements. We have some work to do.

And we’ve made real progress toward new levels. The game is a bit better than it was, certainly no worse. We’ll do more, and even better, tomorrow.

And see the footnotes.


D2.zip


  1. Did he say “just”? Doesn’t he know that “just” never works in programming?