Today, for fun, I decided to give Decor better query messages. And it was fun..

I was noticing that there are only a few messages that come out when you do the ?? key near Decor items, and they’re not related to the particular image associated with the Decor. It might be amusing to create some more messages and have them relate to the particular kind of Decor. Let’s review how they work.

Every Decor instance has an item, an instance of InventoryItem, to give away. At present, they can only give one item, and that item is only given once. Decor are created by providing an array of InventoryItems and calling this method:

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

function Decor:init(tile, item, kind)
    self.sprite = kind and DecorSprites[kind] or Decor:randomSprite()
    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

Decor don’t really know what kind of thing they are, they just store the sprite. Though it is possible to create a specific kind, they’re currently all random, I think:

function Decor:randomSprite()
    local keys = self:getDecorKeys()
    local key = keys[math.random(1,#keys)]
    return DecorSprites[key]
end

function Decor:getDecorKeys()
    local keys = {}
    for k,v in pairs(DecorSprites) do
        table.insert(keys, k)
    end
    return keys
end

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
}

The messages are produced here:

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

The query method is called from elsewhere. We need not concern ourselves with that. But what we’d like to do is to have an array of messages for each kind of Decor. I think by “kind” i mean the major headings in the DecorSprites table, i.e. Skeleton1 or 2, BarrelEmpty, and so on. The less detailed ones are just in there to skew the odds toward bones.

I think what we have here is a data structure that isn’t serving our needs.

Let’s imagine a design that we might like better. Suppose the random decor selection produced one of the major keys in that table. Note that they are strings. So we would produce, e.g. “Skeleton1”, “BarrelEmpty”, whatever. And we’d still be able to skew the odds toward bones if we wish.

Suppose we call that string “kind” and store it in a member variable of that name. Then we have a separate table of kind->asset, and one of kind->messageTable. We display the sprite from the one table, and select a random message from the other.

We do have some Decor tests:

        _:test("Decor Creation", function()
            local tile = FakeTile()
            local item = "xxx"
            local decor = Decor(tile, item, "Skeleton1")
            _:expect(decor.sprite).is(asset.Skeleton1)
        end)
        
        _:test("Random Decor", function()
            local tile = FakeTile()
            local item = "yyy"
            local runner = nil
            local decor = Decor(tile,item)
            _:expect(decor.sprite).isnt(nil)
        end)
        
        _:test("Decor damage", function()
            local tile = FakeTile()
            local decor = Decor(tile,item)
            local round = FakeCombatRound()
            decor:damage(round, "Ow! Dammit!", 5, "_health")
            local match = appendedText:match("Ow!")
            _:expect(match).isnt(nil)
            appendedText = ""
            decor:doNothing()
            _:expect(appendedText).is("")
            decor:castLethargy(round)
            _:expect(appendedText).is("Fumes! A feeling of lethargy comes over you!")
            _:expect(damageAttribute).is("_speed")
            decor:castWeakness(round)
            _:expect(appendedText).is("A poison needle! Suddenly you feel weakened!")
            _:expect(damageAttribute).is("_strength")
        end)
        
        _:test("Decor gives item only once", function()
            local tile = FakeTile()
            local item = FakeItem()
            local decor = Decor(tile,item)
            decor:giveItem()
            _:expect(receivedItem).is("item")
            receivedItem = "nothing"
            decor:giveItem()
            _:expect(receivedItem).is("nothing")
        end)

Those may be good enough as they stand to sustain what we’re doing, except for the message change. I’ll write a test for that when we get there. For now, I want to refactor the data and code to provide for the new functionality.

I’ll start with a simple array of the “kinds”,, with duplicates for the skeletons because I like them.

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

Same count, five each of the skels, one of the others.

Now edit the Sprites table to just have the main keys from above:

local DecorSprites = { 
    Skeleton1=asset.Skeleton1, 
    Skeleton2=asset.Skeleton2,
    BarrelEmpty=asset.barrel_empty, 
    BarrelClosed=asset.barrel_top, 
    BarrelFull=asset.barrel_full,
    Crate=asset.Crate, 
    PotEmpty=asset.pot_empty
}

Now change the init:

function Decor:init(tile, item, kind)
    self.kind = kind or Decor:randomKind()
    self.sprite = DecorSprites[kind]
    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

And replace randomSprite with randomKind:

function Decor:randomKind()
    return DecorKinds[math.random(1,#DecorKinds)]
end

I think this is supposed to work. Test.

One test failed, not communicating much of use:

2: Random Decor  -- Actual: nil, Expected: nil

The test is this:

        _:test("Random Decor", function()
            local tile = FakeTile()
            local item = "yyy"
            local runner = nil
            local decor = Decor(tile,item)
            _:expect(decor.sprite).isnt(nil)
        end)

I am slightly afeared that this test may fail randomly. We’ll see. I enhance it to tell me what kind we’ve got. I suspect I have a key messed up.

        _:test("Random Decor", function()
            local tile = FakeTile()
            local item = "yyy"
            local runner = nil
            local decor = Decor(tile,item)
            _:expect(decor.kind).is("foo")
            _:expect(decor.sprite).isnt(nil)
        end)
2: Random Decor  -- Actual: Skeleton1, Expected: foo

OK, we’re not getting a sprite. Best review the codez.

function Decor:init(tile, item, kind)
    self.kind = kind or Decor:randomKind()
    self.sprite = DecorSprites[kind]
    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

Ah. That should be self.kind:

function Decor:init(tile, item, kind)
    self.kind = kind or Decor:randomKind()
    self.sprite = DecorSprites[self.kind]
    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

Tests run, Game Runs. Commit: Refactor Decor in preparation for individual messages by kind.

Sweet. Now let’s do the messages. That’ll be in query, and we’ll want a table of messages by kind. I’ll do that first.

local DecorMessages = {
    Skeleton1={
    "Yorick: Hamlet? Is that you?",
    "Appears to be some kind of hominid remains.",
    "He's dead, Princess",
    "Bones. Dry, dead, bones.",
    "Seems to have an ID tag ... 'Adventurer something`.",
    "There's a skull. Could be a dead'un. Not sure ...",
    "This fellow's in bad shape, Princess.",
    },
    Skeleton2={
    "Femur, tibia, ..., monster picnic, I'd guess.",
    "Something bad may have happened here. Just guessing.",
    },
    BarrelEmpty={"It's a barrel. It has no top."},
    BarrelClosed={"It's a barrel. Still sealed."},
    BarrelFull={"It's a barrel. Full of some liquid. Might be something in it."},
    Crate={"A crate of some kind. Says 'Ama1 something on the side.",
    "A crate. Probably contains something valuable. Or did."},
    PotEmpty={"A pot. Is there something in it?"}
}

Now query. I said I’d write a test for this and by golly, I will, too.

        _:test("Decor query text", function()
            local tile = FakeTile()
            local item = FakeItem()
            local decor = Decor(tile,item, "BarrelEmpty")
            local msg = decor:query()
            local choices = DecorMessages["BarrelEmpty"]
            _:expect(choices).has(msg)
        end)

This’ll fail.

5: Decor query text  -- Actual: table: 0x291870840, Expected: I am junk.

We code:

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

I decided to provide a default in case we get a weird kind. We’ll do that for the sprite as well, in a moment. I want this test to run now.

And it does. So now I’m going to wander around and see fun messages.

messages

So yes, that’s fun. Let’s see about the sprite. Since I’m in good spirits, I’ll even write a test:

        _:test("Unknown kind gets Skeleton2", function()
            local tile = FakeTile()
            local item = "xxx"
            local decor = Decor(tile, item, "NoSuchKind")
            _:expect(decor.kind).is("Skeleton2")
            _:expect(decor.sprite).is(asset.Skeleton2)
        end)

Test fails of course:

2: Unknown kind gets Skeleton2  -- Actual: NoSuchKind, Expected: Skeleton2
2: Unknown kind gets Skeleton2  -- Actual: nil, Expected: Asset Key: Skeleton2.png (path: "/private/var/mobile/Containers/Data/Application/3739EF5F-0BDD-4A98-99ED-F4CD9ED69217/Documents/D2.codea/Skeleton2.png")

Code:

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

This should run, I reckon. And it does. Commit: Decor items have individualized random query responses.

Now I’m going to add more messages, just for fun. If you want to know what they are, you’ll have to play the game. I warn you, it’s not quite worth buying an iPad …

OK, let’s sum up.

Summary

Nothing to see here. We wanted to add unique messages by kind of decor. We had a few decent tests. We replaced our single table from which we selected a random sprite with a table of “kinds” of decor, a table from kind to sprite, and a table from kind to a list of messages to be used in query.

We adjusted the init and query methods to use those tables, and added a couple of tests to make sure all was well.

I’ve just thought of another kind of test we could do: we don’t have any verification that the tables match up. (We do have some bullet-proofing in the event that they don’t, but why can’t we just go ahead and test for tabular integrity?1

        _:test("Tabular Integrity", function()
            for i,kind in ipairs(DecorKinds) do
                local sp = DecorSprites[kind]
                _:expect(sp, "sprite for "..kind).isnt(nil)
                local msgs = DecorMessages[kind]
                _:expect(msgs,"msgs for "..kind).isnt(nil)
            end
        end)

I add an item “Gronk” to the kinds list and fail to add it to the sprites and messages. Get this:

7: Tabular Integrity sprite for Gronk -- Actual: nil, Expected: nil
7: Tabular Integrity msgs for Gronk -- Actual: nil, Expected: nil

So that’s good. Commit: Tabular integrity now tested for Decor.

A fun morning’s work. It went smoothly enough to make me feel that Decor, at least, has a reasonably simple and sensible design. Would that they were all that way.

See you next time!


D2.zip


  1. Tabular integrity. Is that a great phrase or what??