Planning Meeting. I’m not sure what to do today, but have some ideas.

Yesterday we did a small story, providing ways for the various “speakers” in the dungeon to indicate who was speaking. We have some rudimentary capability installed, including the ability to provide a “speaker” when displaying text, and a vector position from which the text is being emitted. We use that position to draw a “dialog line” from the speaker to the text.

This could certainly be enhanced, but I believe we have all the fundamental capability we need, in that the actual speaker instance is passed all the way down to the Floater, where the graphical decisions are made. We could enhance this further, and could certainly remove a couple of magic numbers, but that doesn’t seem interesting.

Define interesting

Yes, well. My definition of interesting, since we’re not really trying to publish a game, is to think of things to do that will stretch the system’s current capabilities, so as to run into any inadequacies in the design, and to find infelicities in the code and try to improve them. I do this because these issues are like the issues we seem always to encounter in “real” product development. And, frankly, because I enjoy it more than polishing the animations of the monsters or such.

I do know of some issues in addition to all those cards that I listed in yesterday’s article.

  • The Lever can still block the door. The Lever’s function could also be more interesting, and it could be harder to find. Maybe different settings should do different things. Maybe there should be more than one lever.
  • The text scrolling speed seems to be too fast on my maxed-out iPad. It should probably be set to a speed in a way that is not dependent on frame rate. (It might already be independent of frame rate, and just too fast. I don’t memorize this stuff.)

A larger story that might be challenging would be to provide finer-grain control over what goes into the dungeon. Right now, Monsters have a level number, and they won’t be placed in a dungeon whose level is less than their number. That allows at least a simple mechanism for making deeper levels harder.

Beyond that, the things you find in the dungeon are just random, and you can find things that you won’t need, and, for that matter, there are probably things that aren’t even helpful. Does the strength powder do anything? I have my doubts.

So we could provide some perhaps tabular definition of what goes into a level. We could imagine that some Making App that the level design team uses would create that table for us. We could even go hog wild and create some kind of Making App tool for doing it. (I think that’s going to fall into my “not interesting” bucket, because creating complex GUIs isn’t much fun even when it’s valuable.)

A Wild Thought Has Appeared!

An issue related to dungeon definition comes to mind. Levels are currently created by randomly placing rectangular rooms into the available space, and then connecting Room 1 to 2, 2 to 3, 3 to 4, and so on. Since the rooms are placed randomly, that gives some interesting layouts.

Hey, cool! I was going to create some photos of the dungeon with the full map displayed in the mini-map. There is a “Dev Map” switch in the control panel that lets me do that. And it doesn’t work!

Dungeon:292: attempt to index a nil value (field 'map')
stack traceback:
	Dungeon:292: in method 'setVisibility'
	GameRunner:324: in method 'scaleForDeveloper'
	OperatingModes:41: in method 'scaleForLocalMap'
	GameRunner:320: in method 'scaleForLocalMap'
	GameRunner:149: in method 'drawLargeMap'
	GameRunner:128: in method 'draw'
	Main:102: in function 'draw'

Fix the Defect

We have a bug, er, defect! Fortunately, it is in developer mode but still. Let’s see what it is, and whether we can devise a test that would have found it.

function Dungeon:setVisibility()
    for key,tile in self.map:pairs() do
        tile:setVisible()
    end
end

The bug is obvious, we’re supposed to say this:

function Dungeon:setVisibility()
    for key,tile in self:getMap():pairs() do
        tile:setVisible()
    end
end

I will search for any more self.map references in Dungeon. I find it being nilled in init and a reference in createHexMap, which is also flagged to error. I have pretty much decided that hex mapping is less fun than I want to have. This could be chalked up against me as an indication of something that was not planned for, was later contemplated, and found “too hard” to do. A black check mark against incremental design and refactoring, perhaps.

In my defense, I did make it work. Making it work nicely is a pain and while I think we could do it if we had to, it was less fun than I would like to have.

Now should a test have found this self.map reference? Certainly if there was a test for setVisibility it would have caught this.

OK, this irritates me, but I’ll write a test for it.

        _:test("visibility", function()
            for key,tile in dungeon:getMap():pairs() do
                _:expect(tile:isVisible()).is(false)
            end
            dungeon:setVisibility()
            for key,tile in dungeon:getMap():pairs() do
                _:expect(tile:isVisible()).is(true)
            end
        end)

Test runs. Break the method to be sure.

7: visibility -- Dungeon:301: attempt to index a nil value (field 'map')

Put it back. Tests run. Commit: Fix defect where Dev Map switch caused traceback. Test added.

Defect Removed

OK, where were we? Oh, right, I was talking about the dungeon layout maps.

map1

map2

map3

The rooms are laid in randomly, and can be adjacent but not overlapping. They are connected, not nearest to nearest, but in numeric order 1-2, 2-3, 3-4. Connections are center to center, randomly horizontal then vertical or vertical then horizontal. If you look carefully at the maps above, you can usually figure out which rooms a given hallway really connects, but of course the hallways can cut through other rooms, and often do.

I really like the randomness that this gives to the dungeon, but it does make for problems in setting up traps and treasures and such. Let’s make up a scenario.

Suppose the WayDown on some level is protected by a DeathTrap. And suppose that there is a Button that can be pressed to disable the DeathTrap. And suppose that we want to ensure that the Princess can always find the Button before she has to cross the DeathTrap. Other than putting the Button in Room 1, where she spawns, there’s no easy way to make sure that there is a trap-free path to the Button. And putting it in Room 1 isn’t much of a puzzle.

There are other similar issues, and there’s some fairly odd code in the game to deal with it. Here’s how we create Things:


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

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

function DungeonBuilder:randomRoomTileAvoidingRoom(roomToAvoid)
    return self:dungeonAnalyzer():randomRoomTileAvoidingRoom(roomToAvoid)
end

function DungeonAnalyzer:randomRoomTileAvoidingRoom(roomToAvoid)
    local x1,y1, x2,y2 = 0,0,0,0
    if roomToAvoid then
        x1,y1, x2,y2 = roomToAvoid:corners()
    end
    return self:randomRoomTileAvoidingCoordinates(x1,y1, x2,y2)
end

function DungeonAnalyzer:randomRoomTileAvoidingCoordinates(x1,y1,x2,y2)
    local x,y, pos, tile
    repeat
        pos = self:randomPositionAvoiding(x1,y1, x2,y2, self.tileCountX, self.tileCountY)
        tile = self.finder:getTile(pos.x,pos.y)
    until tile:isOpenRoom()
    return tile
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 or not r:isEmpty() then return false end
        end
    end
    return true
end

Programmers like me will enjoy tracing this down to the end. More procedurally-inclined programmers may experience dizziness or confusion. But it all comes down to “find a tile that is an empty room tile, and that is surrounded by empty room tiles (and isn’t in the designated room if we designated one).

The in-game effect of this code is to place Things (whatever they are) in open space. They’ll never be against a wall, which is one way of ensuring that they never block a doorway. (There could possibly be a better way. We kind of need it for the Lever, except that we’ll probably change how we place it anyway.)

A “Big” Solution

We do have the ability to find the shortest path between points in the Dungeon. We use it to compute the path that the PathFinder cloud creature will use to guide the Princess to the WayDown. There’s probably some way that we could use that path algorithm to ensure that there is a way to get from Room 1, where we start, to the Button, without passing through the DeathTrap.

At some expense in dungeon creation time, we could randomly place the Button, use the path function to find a path to it, treating the DeathTrap as if it were a wall. If we can’t find a path, roll a new random place for the Button.

What I don’t love about this idea is that it might be tricky to set up. But what I do love about it is that it can probably be made to work for any situation we’re likely to come up with. And it can surely be packaged up so that all the pathfinding comes down to returning a boolean saying whether we could find a path or not. We could even use the path to place something, if we wanted to.

Where Does This Lead?

I think this leads to an approach to the dungeon design issue that will give us a fairly decent general approach. We might still want a given level to have a specific layout, as we do for the Learning Level, but we can deal with that as a separate request.

Let’s move back to today.

Scrolling Speed

Let’s have a look at how the Floater handles scrolling speed. And should I change its name? 😀

On every draw cycle, we call this:

function Floater:increment(n)
    self.yOff = self.yOff + (n or self:adjustedIncrement())
    if self:linesToDisplay() > self.lineCount then
        table.remove(self.buffer,1)
        self.yOff = self.yOff - self.lineSize
    end
    if #self.buffer < self:linesToDisplay() then
        self:fetchMessage()
    end
    assert(#self.buffer~=0, "Floater buffer is empty!")
end

function Floater:adjustedIncrement()
    return 1.5
    --if DeltaTime > 0.02 then return 2 else return 1 end
end

A quick glance at that tells me why scrolling speed varies with your device speed. We’re not looking at the DeltaTime, the time between draw calls. The return value is in coordinates, i.e. pixels, which I guess is fine.

DeltaTime can be approximately 1/30th of a second, 1/60th, and probably other values like that. Codea tries to cycle as fast as it can and stops down to the next slower cycle if a draw takes longer than the current cycle time.

I have to do some math here. When DeltaTime is small, we want the adjustedIncrement to be small. When DeltaTime is larger, adjustedIncrement should be proportionately larger.

So it seems to me we want something like this:

    return baseValue*DeltaTime

Where baseValue is approximately 1 pixel per sixtieth of a second, because I figure this iPad is probably running in 60ths.

I’ll try this:

function Floater:adjustedIncrement()
    local base = 40.0
    return base*DeltaTime
end

So 40 here means 40 pixels in one second, if I understand what I’ve done. The display looks better, but a couple of tests are complaining:

3: floater pulls messages appropriately  -- Actual: 1, Expected: 2
3: floater pulls messages appropriately -- TestFloater:58: attempt to index a nil value (field 'integer index')

These errors aren’t helping me. I can’t even see what the first one is. What is at TestFloater:58?

            fl:increment(24)
            _:expect(#fl.buffer).is(1)
            _:expect(fl.buffer[1]:message()).is("Message 1")
            fl:increment()
            _:expect(#fl.buffer).is(2)
-- line below is 58
            _:expect(fl.buffer[2]:message()).is("Message 2")
            fl:increment(25)
            _:expect(fl.buffer[3]:message()).is("Message 3")

I notice the call to increment with no parameter, which will surely return something odd. The increment function allows me to provide a test value to use, and this call didn’t specify. That tells me that it will have had 1.5 return. Let’s try that. The tests pass.

This test is very fragile, because neither 1 nor 2 will pass, but 1.5 does. We’ll accept that for now. I decide to set the speed to 30. I’m a fast reader, so I can probably tolerate a faster scroll than some folks. Also, I know what it’s going to say. Commit: Crawl speed is 30 pixels per second, depends on DeltaTime.

I don’t like the dialog line. It’s a bit too bright. Let me fiddle a bit.

function Floater:drawMessage(op, pos)
    local txt = op:message()
    local origin = op:speechOrigin(pos)
    strokeWidth(1)
    if origin.x ~= 0 then
        line(origin.x, origin.y, pos.x,pos.y-8)
    end
    text(txt, pos.x, pos.y)
end

The strokeWidth was 3. Set to 1, it looks better.

thinner dialog lines

Commit: Thinner dialog line, strokeWidth 1.

This Has Been Fun And All …

But aside from a vague wave at planning, asking for a better way to lay out the dungeon objects, what have I done for me lately?

Well, I found a fixed a defect, improved the crawl speed, and improved the look of the dialog line. Nothing big, I grant.

But we also did some speculative but productive thinking about object placement, which is a story that seems to be coming up. In aid of that let’s review a bit more code.

The dungeon contents are done like this:

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)
    self:placeWayDown()
    self:setupMonsters()
end

There are all those different things, notably Loots, Decor, and Things. What’s the difference among those?

function DungeonBuilder:createDecor(n)
    local sourceItems = {
        InventoryItem("cat_persuasion"),
        --
        InventoryItem("curePoison"),
        InventoryItem("pathfinder"),
        InventoryItem("rock"),
        --
        InventoryItem("nothing"),
        InventoryItem("nothing"),
        InventoryItem("nothing")--]]
    }
    local items = {}
    for i = 1,n or 10 do
        table.insert(items, sourceItems[1 + i%#sourceItems])
    end
    Decor:createRequiredItemsInEmptyTiles(items,self)
end

function Decor:createRequiredItemsInEmptyTiles(items, runner)
    return ar.map(items, function(item)
        local tile = runner:randomRoomTileAvoidingRoomNumber(666)
        return Decor(tile,item)
    end)
end

What we have here is a vague attempt to randomize what we create.

function Decor:init(tile, item, kind)
    self.kind = kind or Decor:randomKind()
    self.sprite = DecorSprites[self.kind]
    if not self.sprite then
        self.kind = "Skeleton2"
        self.sprite = DecorSprites[self.kind]
    end
    self.item = item
    -- self.tile = nil -- tile needed for TileArbiter and move interaction
    tile:moveObject(self)
    self.scaleX = ScaleX[math.random(1,2)]
    local dt = {self.doNothing, self.doNothing, self.castLethargy, self.castWeakness}
    self.danger = dt[math.random(1,#dt)]
end

We create a random kind of Decor, which is selected from this list:

local DecorKinds = {
    "Skeleton1","Skeleton1","Skeleton1","Skeleton1","Skeleton1",
    "Skeleton2","Skeleton2","Skeleton2","Skeleton2","Skeleton2",
    "BarrelEmpty",
    "BarrelClosed",
    "BarrelFull",
    "Crate",
    "PotEmpty",
    "PotFull",
}

The multiple occurrences of skeletons ensure that there are lots of skeletons around relative to other objects.

These objects wind up “wrapping” the items to be provided, which was set up in createDecor, the curePoison and such.

We might want to explore how InventoryItems work. In essence, they are looked up in the InventoryTable by name.

local ItemTable = {
    pathfinder={ icon="blue_jar", name="Magic Jar", attribute="spawnPathfinder", description="Magic Jar to create a Pathfinding Cloud Creature" },
    rock={ icon="rock", name="Rock", attribute="dullSharpness", description="Mysterious Rock of Dullness", used="You have mysteriously dulled all the sharp objects near by." },
    curePoison={ icon="red_vase", name="Poison Antidote", attribute="curePoison" },
    health={icon="red_vial", name="Health", description="Potent Potion of Health", attribute="addPoints", value1="Health", value2=1},
    strength={icon="blue_pack", name="Strength", description="Pack of Steroidal Strength Powder", attribute="addPoints", value1="Strength", value2=1},
    speed={icon="green_flask", name="Speed", description="Spirits of Substantial Speed", attribute="addPoints", value1="Speed", value2=1},
    cat_persuasion = {icon="cat_amulet", name="Precious Amulet of Feline Persuasion", attribute="catPersuasion"},
    testGreen={icon="green_staff"},
    testSnake={icon="snake_staff"},
}

I think we need not explore that just now. Let’s look at Loots.

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

And …

local RandomLootInfo = {
    {"strength", 4,9},
    {"health", 4,10},
    {"speed", 2,5 },
    {"pathfinder", 0,0},
    {"curePoison", 0,0}
}

~~~lua
Loot = class(DungeonObject)

local LootIcons = {strength="blue_pack", health="red_vial", speed="green_flask",
pathfinder="blue_jar", curePoison="red_vase"}

local LootDescriptions = {strength="Pack of Steroidal Strength Powder", health="Potent Potion of Health", speed="Potion of Monstrous Speed" }

function Loot:init(tile, kind, min, max)
    self.kind = kind
    self.icon = self:getIcon(self.kind)
    self.min = min
    self.max = max
    self.desc = LootDescriptions[self.kind] or self.kind
    self.message = "I am a valuable "..self.desc
    if tile then tile:moveObject(self) end
end

Loot is just found lying about, all on its own, not enclosed in Decor.

It sure seems like those ideas could be combined, perhaps with a kind of Thing container that is invisible, And the mapping between these things and the Decor isn’t clear. Is it possible to put all of these items in Decor, or not?

Oh Wonderful

What we have here is another refactoring opportunity. I believe that we should actually think a bit. Well, I’m always in favor of thinking, but what I mean here is that we should think about the conceptual issues here and let that thinking guide our refactoring.

It seems to me offhand that there are containers, called Decor, that may contain valuable items. At least some of those items can also appear just lying about as Loot. Health potions can do that, I think. There is also the matter of Chests, which are separate for some reason, but certainly can contain Health potions. (Do they ever contain anything else? I suspect not.)

So we should think about what we really want. We’ve got a few different ideas implemented in a somewhat ad-hoc style. I think I’d even like to draw a picture or two that will tell me what is done in here. The programmers of this system seem to have missed an opportunity to remove conceptual duplication, although there may not be much code duplication per se.

That’s interesting, isn’t it? There’s duplication here that may not show up as lines of code duplicated, nor even lines of code run more times than necessary, but just similar ideas that we now see could probably be merged.

This is true “technical debt”, the difference between what we understood in the past and what we understand now.

I’m glad we had this little chat. We’ve discovered an opportunity to improve our design due to better understanding than we had. Of course, to do that, we need to generate that better understanding.

That will be fun. We’ve started already. We’ll do more tomorrow, GWATCDR.

Lessons?

Well, there’s the lesson about testing even trivial code like the loop setting visibility. While greater care would have allowed me to notice that self.map a simple test would have guaranteed that it came to my attention when I changed how the map is accessed.

There’s the value of thinking about a problem that pops into our mind. I had a vague concern about the randomness of the dungeon making it hard to allow more control over how things are laid in. I wanted to look at some layouts … that got me thinking about paths … and that gives me confidence that as we work on layout, we’ll be able to resolve even some difficult issues like making sure you can get to a key before you absolutely must have a key.

And then, in looking at the Decor and Loots and such. we noticed some conceptual similarities that are not reflected in similar code—or code that is similar enough. And that makes us realize that we might profit from thinking a bit more generally about how all these things work, and how they can be made more similar.

And you know what? I bet we’re going to find ways to get from where we are, nearer to where we’d like to be, in tiny little steps.

Want to get any money down on the don’t? Because I could use a treat, and if you bet against me on this I’m gonna take your money.

Stop by next time and see.



D2.zip