It’s interesting how one idea leads to another. I’m sure I have too many ideas now, but some of them seem interesting. The dungeon objects come to life!

If you had asked me yesterday what I would do today, I’d probably have said that I’d work on putting valuable items into Decor, not just poison. I might have mentioned that I didn’t really want to make a unique class for each kind of Decor, but that I’d probably want some kind of table to say what was in there. I would probably have mentioned that I thought that almost everything in the dungeon should be an inventory item rather than a simple power-up as most things are now.

But I was musing this morning, between the time the cat stood on me and the time I actually got up, and now I have more ideas than I had, and perhaps more than I need. Ideas include:

Information and Conversation
Add a button to the screen containing a question mark. When you press the question mark, and you are close enough to a monster, decor, or loot, you’ll get a message in the crawl telling you something about it.

There could, and often should be more than one message. “Seems to be a pile of bones”, “There’s something covered by the bones”, “You’ve found an Amulet of Perpetual Noodling!” After some of those methods, the item might give you something, exact some penalty, who knows what.

Inventory Duplicates and Information
As we add more inventory items, the player will need to know what they are, and we will need to allow for more than one of a given item. We should probably move the inventory over to the left side of the screen, in rows, including item count and name.
Memory
Any item that you can interact with should have the ability to maintain enough state to know that it has already done for you what it’s going to do, so that it at least drops into a final state, like “You’ve found the same old bones again”.
Quests
We could have a monster who assigns a quest. “Find and return to me my lost Jewel of Inherent Redness”. “Ah, you have found my precious Jewel. As your reward, please accept this Donut of Great Caloric Content.”
Scripts and Set Pieces
We’ll probably want to have reasonably fine control over where things go, what exchanges occur at what levels, and so on.
Get Real
We need to keep in mind that almost no one is ever going to play this game, unless someone steps up to help me convert it to Xcode so that it can go on the App Store. We’ll want to focus our attention on having substantial capability, but we probably won’t generate large numbers of layers, interactions, exchanges, or set pieces.

So that’s a lot. How might we do it?

How Might We Do This?

Codea Lua has approximately two structures to help us. It has tables, which can act like arrays or like hashmaps, and it has classes, which are tables containing pointers to functions, together with some syntactical sugar to make function calls act like message sends.

Beyond that, if you want it, you build it. (Of course there are libraries of supposedly useful stuff. We won’t likely go there.)

We already have a few rudimentary examples of what we might do.

Decor

Decor setup is table driven:

local DecorSprites = { Skeleton1=asset.Skeleton1, Skeleton2=asset.Skeleton2,
s11=asset.Skeleton1, s12=asset.Skeleton1, s13=asset.Skeleton1, s14=asset.Skeleton1,
s21=asset.Skeleton2, s22=asset.Skeleton2, s23=asset.Skeleton2, s24=asset.Skeleton2,
BarrelEmpty=asset.barrel_empty, BarrelClosed=asset.barrel_top, BarrelFull=asset.barrel_full,
Crate=asset.Crate, PotEmpty=asset.pot_empty
}

This table is accessed randomly to create decor items. The multiple occurrences of skeleton are in there to bias the selection toward skeletons.

Tile Access

Access to tiles is table driven, as we saw yesterday. The TileArbiter looks up behavior in its table:

    local t = {}
    t[Chest] = {}
    t[Chest][Monster] = {moveTo=TileArbiter.refuseMove}
    t[Chest][Player] = {moveTo=TileArbiter.refuseMove, action=Player.startActionWithChest}
    t[Decor] = {}
    t[Decor][Player] = {moveTo=TileArbiter.acceptMove, action=Player.startActionWithDecor}

There’s a bit more logic here. If a resident-mover pair has a moveTo key in its table, the value at that key will be called. If it has an action key, that action will be called during the move.

Monster Definition

Monsters have substantial control tables:

    MT = {}
    m = {name="Pink Slime", level = 1, health={1,2}, speed = {4,10}, strength=1,
    attackVerbs={"smears", "squishes", "sloshes at"},
    dead=asset.slime_squashed, hit=asset.slime_hit,
    moving={asset.slime, asset.slime_walk, asset.slime_squashed}}
    table.insert(MT,m)
    m = {name="Death Fly", level = 1, health={2,3}, speed = {8,12}, strength=1,
    attackVerbs={"bites", "poisons"},
    dead=asset.fly_dead, hit=asset.fly_hit,
    moving={asset.fly, asset.fly_fly}}
    table.insert(MT,m)
    m = {name="Ghost", level=1, health={1,5}, speed={5,9},strength={1,1},
    attackVerbs={"licks", "terrifies", "slams"},
    dead=asset.ghost_dead, hit=asset.ghost_hit,
    moving={asset.ghost, asset.ghost_normal}}
    table.insert(MT,m)

The table provides lots of information, including a list of verbs to use when the monster attacks, so that it doesn’t always say the same things. The table provides the sprites that are used to draw the monster, and if there’s more than one sprite under the moving key, the monster cycles those sprites, which is what makes the monsters move a bit rather than just stand there idle.

Monster Strategy

Monsters have a few basic strategies, including Calm monsters that hang around you but leave you alone if you leave them alone, Nasty monsters that will attack you if you come into their range, Hangout monsters that stay near a given point, and Path monsters that follow a path. These strategies are implemented with small classes, using a standard protocol. When it is time for a monster to move, the game calls execute on its movement strategy. That method does whatever it needs to do to select the tile it wants to move to.

One More Thing …

Functions are first-class objects in Lua, so a function can be stored in a variable or table. Because I am pretty much an object-oriented kind of guy, I don’t use this capability often, but we should keep in mind that we could use functions in this fashion.

I am usually more inclined to save the name of a method, but we could save a method function, or even a naked function, if we wish.

But we still don’t know …

How Shall We Set Out to Do This?

I think we’re moving in a direction with some real potential, but depending on just what we do, it could change gameplay. Right now, to interact with something you try to move onto its square. We’re proposing to add a new button, “?”, that triggers an interaction. I guess it’s an open question whether moving onto the square is the same as “?” or not. It might be that when you move without questioning, a particular action, possibly the most harmful, is taken. Or we could make it just the same as “?”.

With that in mind … let’s make it the same as “?”, and that will let us defer the implementation of a new button.

No, I think the “?” button will have some interesting aspect, like deciding what we’re asking about.

Let’s change one of our existing buttons to “?” for now. I choose “Flee”, which has always been our experimental button anyway.

function GameRunner:createCombatButtons(buttons)
    table.insert(buttons, Button:textButton("??", 100,350))
    table.insert(buttons, Button:textButton("Fight",200,350))
end

function Button:textButton(label, x, y, w, h)
    local w = w or 64
    local h = h or 64
    local img = Button:createTextImage(label,w,h)
    if label == "??" then label = "query" end
    return Button(string.lower(label), x, y, w, h, img)
end

We use the label to call the method, and I am not inclined to name a method ??, so I convert that to query. Now I need a query method on player.

function Player:query()
    self:inform("What are you asking about now?")
end

I think this should hook up. Bizarrely, the buttons don’t show up at all. What have I broken? I change the button back to Flee and it works. I change it to ??? and it works. I change it back to ?? … and it works. Bizarre. Anyway it’s hooked up:

query

Now what? I think that since we don’t know which way she’s looking, we should just pick the closest thing and send it a message.

Sounds like a job for Tile: return nearest contents. I’m not entirely happy here, because Tile knows a lot but it seems close to right. We’ll try this:

function Player:query()
    local contents = self.tile:nearestContents()
    contents:query()
end

In Tile, I think we want to defer this to Dungeon, which has a lot of smarts about nearby tiles.

function Tile:nearestContents(aTile)
    self.runner:getDungeon():nearestContents(aTile)
end

I’m finding this a bit dithery. The responsibilities are always a step away from where I look. Anyway, the player knows her tile, and the tile dungeon seems like a good place to do tile group work. We’ll start there and if it seems we should, we’ll move things around.

I’ve coded this but I really think I should have TDD’d it. I’m going to test, once. As expected, I need more than one try. And it still doesn’t work. Let’s write a test.

Well, this is embarrassing. I have not been able to get the test to run, but the code runs. Here’s the test in its final form:

        _:test("neighbors", function()
            local tile = Tile:room(10,10,Runner)
            local t9 = Tile:room(9,9,Runner)
            local decor = Decor(t9,Runner)
            dungeon = Runner:getDungeon()
            _:expect(decor.query, "understands query").isnt(nil)
            local contents = t9:getContents()
            _:expect(contents:contains(decor),"check contents method").is(true)
            _:expect(contents, "contents has decor").has(decor)
            local neighbors = dungeon:neighbors(tile)
            _:expect(#neighbors).is(8)
            _:expect(neighbors, "same t9").has(t9)
            local queryable = dungeon:nearestContents(tile)
            _:expect(queryable).is(t9)
        end)

It gets two failures:

6: neighbors same t9 -- Actual: table: 0x28439f880, Expected: Tile[9][9]: room
6: neighbors  -- Actual: nil, Expected: Tile[9][9]: room

What is happening is that the tiles we’re seeing in the code are not the tiles we set up in the test. The test was useful enough that I managed to make the code actually work, as I’ll show in a moment. Let’s try a tweak to the test to see if we can get it to run.

        _:test("neighbors", function()
            local tile = Tile:room(10,10,Runner)
            local t9 = dungeon:getTile(vec2(9,9))
            local decor = Decor(t9,Runner)
            dungeon = Runner:getDungeon()
            _:expect(decor.query, "understands query").isnt(nil)
            local contents = t9:getContents()
            _:expect(contents:contains(decor),"check contents method").is(true)
            _:expect(contents, "contents has decor").has(decor)
            local neighbors = dungeon:neighbors(tile)
            _:expect(#neighbors).is(8)
            _:expect(neighbors, "same t9").has(t9)
            local queryable = dungeon:nearestContents(tile)
            _:expect(queryable).is(decor)
        end)

That passes. Instead of creating a new tile for t9, I fetched the existing one and gave it the desired contents. Then I changed the last expect to be correct, looking for decor, not t9. Now to remove some of the stuff I used to figure out why it wasn’t working.

        _:test("neighbors", function()
            local tile = Tile:room(10,10,Runner)
            local t9 = dungeon:getTile(vec2(9,9))
            local decor = Decor(t9,Runner)
            dungeon = Runner:getDungeon()
            local neighbors = dungeon:neighbors(tile)
            _:expect(#neighbors).is(8)
            _:expect(neighbors, "same t9").has(t9)
            local queryable = dungeon:nearestContents(tile)
            _:expect(queryable).is(decor)
        end)

OK, that tells the story I’m trying to tell. Good enough for now. Here’s the code for that, which is fairly decent:

function Dungeon:nearestContents(tile)
    local neighbors = self:neighbors(tile)
    for i,tile in ipairs(neighbors) do
        for k,obj in pairs(tile:getContents()) do
            local q = obj.query
            if q then return obj end
        end
    end
    return nil
end

function Dungeon:neighbors(tile)
    local tPos = tile:pos()
    local offsets = self:neighborOffsets()
    return map(offsets, function(offset) return self:getTile(offset + tPos) end)
end

function Dungeon:neighborOffsets()
    return { 
    vec2(-1,-1), vec2(0,-1), vec2(1,-1),
    vec2(-1,0),              vec2(1,0),
    vec2(-1,1),  vec2(0,1),  vec2(1,1)
    }
end

The neighborOffset method was there. The neighbors method maps a collection of offsets to a collection of tiles. And the nearestContents method just returns the first contents item it finds that responds to the method query.

If we don’t find one, we’re returning nil. That means that the Player had to check it. Instead let’s make an object that we can return.

NullQueryObject = class()

function NullQueryObject:query()
    return "No response"
end

And we use it:

function Dungeon:nearestContents(tile)
    local neighbors = self:neighbors(tile)
    for i,tile in ipairs(neighbors) do
        for k,obj in pairs(tile:getContents()) do
            local q = obj.query
            if q then return obj end
        end
    end
    return NullQueryObject()
end

And we rely on that in Player:

function Player:query()
    self:inform(self.tile:nearestContents():query())
end

who's asking

So that’s nearly good. Let’s put a query method on monsters.

No, let’s commit first: New ?? button queries nearby objects. Decor has trivial query response.

Now then:

function Monster:query()
    return "I'm a "..self:name()
end

This works, as one might imagine. One more change for now, to make the answer from Decor better.

function Decor:query()
    local answers = {"I am junk.", "I am debris", "Just stuff.", "Detritus"}
    return answers[math.random(1,#answers)]
end

No, I’m on a roll. Let’s do loot.

I wind up doing lots of them:

function WayDown:query()
    return "I am the Way Down! Proceed with due caution!"
end

function Monster:query()
    return "I'm a "..self:name()
end

function Loot:query()
    return "I am a valuable "..self.kind
end

function Key:query()
    return "Could I possibly be ... a key???"
end

function Chest:query()
    return "I am probably a mysterious chest."
end

function Spikes:query()
    return "I am Deadly Spikes, what do I look like??"
end

function Decor:query()
    local answers = {"I am junk.", "I am debris", "Oh, just stuff.", "Well, mostly detritus, some trash ..."}
    return answers[math.random(1,#answers)]
end

These will all need enhancement but now we have a fairly nice query function. Commit: Most objects respond sensibly to the ?? button.

lots

Let’s Sum Up

So that went mostly nicely, after a bit of struggle plugging in the basic capability. I had several typos and brain flatulencies that I didn’t share in the interest of not appearing to be a complete fool. Not that I’m not a fool, but I’m not a complete one. For example, I have no appendix.

But it did go well, as soon as I got around to actually returning the right result from thenearestContents method.

We can see what we will be wanting in the future, which will be a way to give Decor a sensible name, such as Loot has. And I am very tempted to actually remove the feature of stepping onto a tile to try to attack or take it. We’ll see. That might make the game less free-flowing and fun.

As for our grand plans at the beginning of the article, we have as usual managed to get quite a lot of decent functionality without a lot of infrastructure or refactoring. This is a good thing, but we will want to look around to see where things can be improved and simplified.

I’m still feeling that the GameRunner v Dungeon v Tile allegiance is too complicated, but it’s not terrible. I suspect that if a tile knew a dungeon instead of a runner, we might be better off. I feel that Tiles are central to much of the design but that the GameRunner gets in there as well. And Dungeon is still just coming into its own, although it now contains a number of sophisticated and important capabilities.

All in, a good morning, with some cool new stuff.


D2.zip