A Quest! Let’s have a Quest.

Just to force our developers (me) to work on something new, our Product Owner (me) has asked us to develop an initial version of a Quest. Let’s imagine what that might be:

Suppose we put an entity or object somewhere in the dungeon. Call it NPC. Maybe it just stands somewhere, maybe it moves. If you use the ?? near it, it will somehow indicate that it has lost some valuable thing, and ask that if you find it, you’ll bring it back.

Possibly it will pop up some kind of simple dialog allowing you to pick from a short list of things to say to it. Possibly it will say different things depending on what you say.

If you’ve seen the request to find the thing, and you find the thing, and you come back to the entity, and you deploy the thing from your inventory, the NPC will thank you, and give you some object of great value, such as an item that can dispel monsters, or one that can reveal what’s inside a Decor without taking the hit from it.

How might we do this? I wish you were here to chat with me about it, but let’s speculate a bit about approaches.

Maybe the NPC is a kind of Monster. Maybe NPC should be a new subclass of Entity, with its own behavior. Are we drawing Monsters explicitly now, or are we just drawing them as part of Tile contents drawing? I think it’s the latter. We can check if we need to know.

If NPC is a new class, it’ll be relatively easy to implement the query method to produce varying messages depending on NPC state, and, probably, to pop up a conversation dialog. In addition, Entity subclasses have a lot of assumed behavior, like combat attributes, that may not be suitable for the NPC.

It might be best to derive it directly from DungeonObject, and if it seems to share a lot of behavior with other objects, remove the duplication in some suitable way. This is the way.

Maybe a conversation dialog is a largeish window that contains what the NPC says at the top, and then a few optional answers you can give. The NPC will have some kind of state-driven behavior telling it what to say next.

We have no really useful way to let the player type answers into Codea. Perhaps in some future version. That would probably be nice. A dialog should work for now.

We may need to do something to ensure that while all this is going on, no monsters leap out and attack or anything that requires attention.

Is an NPC like a Chest? Is it like Decor?

Let’s just dive in. We’l derive it from DungeonObject, because it is surely at least some kind of tile contents. There’s not much behavior at all in DungeonObject.

I’ll set up some TDD for the object, but the first thing I’ll try to do is get it to stand near the Player’s starting location and draw itself.

NPC

I begin with a simple trivial class and test:

function testNPC()
    CodeaUnit.detailed = false
    
    _:describe("NPC", function()
        
        _:before(function()
        end)
        
        _:after(function()
        end)
        
        _:test("First Test", function()
            _:expect(1).is(2) -- change this to fail
        end)
        
    end)
end

NPC = class(DungeonObject)

function NPC:init()
end

function NPC:draw()
    -- Codea does not automatically call this method
end

EXPLETIVE! Broken tests! I was certain there weren’t any.

I can’t tell you how much this irritates me. I really hate it when I screw up and it seems to be due to not having run the tests. I was dead certain that I had done so, many times.

OK, enough blaming, let’s find that problem.

1: add items to inventory -- Inventory:173: table index is nil
4: Inventory Item equality wilma -- Actual: table: 0x281ab2d80, Expected: table: 0x281ab2f00
5: Inventory structure -- Inventory:173: table index is nil
1: First Test  -- Actual: 1, Expected: 2

That last one is the one from NPC. The others wtf.

function Inventory:put(entry)
    if entry.count > 0 then
        inventory[entry.name] = entry --< -- 173
    else
        inventory[entry.name] = nil
    end
    self:makeLinear()
end

OK, those are surely my fake items.

        _:test("add items to inventory", function()
            local i1 = InventoryItem{icon="green_staff"}
            local i2 = InventoryItem{icon="snake_staff"}
            _:expect(Inventory:count()).is(0)
            Inventory:add(i1)
            _:expect(Inventory:count()).is(1)
            Inventory:add(i2)
            _:expect(Inventory:count()).is(2)
            local i3 = InventoryItem{icon="green_staff"}
            Inventory:add(i3)
            _:expect(Inventory:count()).is(2)
        end)

Oh come on! How could anyone, even someone as dull as I am, miss that these were failing? You can’t even create an inventory item that way any more:

function InventoryItem:init(aString, aValue2)
    local entry = InventoryTable:getTable(aString)
    self.icon = entry.icon
    self.name = entry.name or self.icon
    self.description = entry.description or self.name
    self.used = entry.used or nil
    self.attribute = entry.attribute or nil
    self.value1 = entry.value or entry.value1 or nil
    self.value2 = aValue2 or entry.value2 or nil
end

This test couldn’t possibly have run. I am dismay. Anyway let’s add a couple of test items to the table.

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},
    testGreen={icon="green_staff"},
    testSnake={icon="snake_staff"},
}

Use in the test:

        _:test("add items to inventory", function()
            local i1 = InventoryItem("testGreen")
            local i2 = InventoryItem("testSnake")
            _:expect(Inventory:count()).is(0)
            Inventory:add(i1)
            _:expect(Inventory:count()).is(1)
            Inventory:add(i2)
            _:expect(Inventory:count()).is(2)
            local i3 = InventoryItem("testGreen")
            Inventory:add(i3)
            _:expect(Inventory:count()).is(2)
        end)

Test runs. Next:

4: Inventory Item equality wilma -- Actual: table: 0x281a81a00, Expected: table: 0x281a826c0
        _:test("Inventory Item equality", function()
            local i1 = InventoryItem("testGreen")
            local i2 = InventoryItem("testGreen")
            local i3 = InventoryItem("testSnake")
            _:expect(i1, "green ~= snake").isnt(i3)
            _:expect(i1,"two greens").is(i2)
        end)

That runs. Now:

5: Inventory structure -- Inventory:175: table index is nil
        _:test("Inventory structure", function()
            local i1 = InventoryItem{name="fred"}
            local i2 = InventoryItem{name="fred"}
            local i3 = InventoryItem{name="wilma"}
            Inventory:clear()
            Inventory:add(i1)
            Inventory:add(i2)
            Inventory:add(i3)
            local fredCount = Inventory:howMany(i1)
            _:expect(fredCount, "freds").is(2)
            local items = Inventory:entries()
            _:expect(#items).is(2)
            _:expect(Inventory:entries()).is(Inventory:entries())
            local wilmaCount = Inventory:howMany(i3)
            _:expect(wilmaCount, "wilmas").is(1)
            Inventory:remove(i1)
            _:expect(Inventory:howMany(i1),"fewer freds").is(1)
            Inventory:remove(i1)
            _:expect(Inventory:howMany(i1),"no freds").is(0)
            _:expect(#Inventory:entries()).is(1)
        end)

I’ll convert this to use the green and snake items. The edit will be a bit tricky. green=fred, snake=wilma. I passed this over to Sublime for some slick multi-cursor action:

        _:test("Inventory structure", function()
            local i1 = InventoryItem("testGreen")
            local i2 = InventoryItem("testGreen")
            local i3 = InventoryItem("testSnake")
            Inventory:clear()
            Inventory:add(i1)
            Inventory:add(i2)
            Inventory:add(i3)
            local greenCount = Inventory:howMany(i1)
            _:expect(greenCount, "greens").is(2)
            local items = Inventory:entries()
            _:expect(#items).is(2)
            _:expect(Inventory:entries()).is(Inventory:entries())
            local snakeCount = Inventory:howMany(i3)
            _:expect(snakeCount, "snakes").is(1)
            Inventory:remove(i1)
            _:expect(Inventory:howMany(i1),"fewer greens").is(1)
            Inventory:remove(i1)
            _:expect(Inventory:howMany(i1),"no greens").is(0)
            _:expect(#Inventory:entries()).is(1)
        end)

And we’re back to just the failing test I expected. I’m going to fix that and export a new version for yesterday’s article.

OK, that’s exported. After the web site compiles, I’ll push it up so that no users are impacted by my mistake. Also, no users are using the zip files at all. My audience … where is it?

If you can see this, please tweet me up or something!

NPC Again

Now then, back to NPC. We have special code in GameRunner that currently puts a Darkness into the first room. We don’t have a story for that yet. So let’s change this:

function GameRunner:placeNPC()
    local r1 = self.rooms[1]
    local tile = r1:centerTile()
    local npcTile = tile:getNeighbor(vec2(2,0))
    NPC(npcTile)
end

This is called in createLevel. Trust me on that. NPC must be a bit like Darkness, let’s look and see how they did that one.

Darkness = class(DungeonObject)

function Darkness:init(tile)
    tile:moveObject(self)
    self.sprite = asset.button_up_off
end

function Darkness:actionWithPlayer(aPlayer)
    Bus:publish("darkness")
    self.sprite = asset.button_down_on
end

function Darkness:draw(tiny, center)
    if tiny then return end
    pushMatrix()
    translate(center.x, center.y)
    sprite(self.sprite,0,0, 50,50)
    popMatrix()
end

Makes sense. Let’s just paste that over and then debate about duplication.

My transcription of that is this:

function NPC:init(tile)
    tile:moveObject(self)
    self.sprite = asset.builtin.Planet_Cute.Character_Horn_Girl
end

function NPC:actionWithPlayer(aPlayer)
    print("NPC saw that")
end

function NPC:draw(tiny, center)
    if tiny then return end
    pushMatrix()
    translate(center.x, center.y)
    sprite(self.sprite,0,0, 50,50)
    popMatrix()
end

I expect that we need a TileArbiter entry, and that it’ll be like that of Darkness:

    t[Darkness] = {}
    t[Darkness][Monster] = {moveTo=TileArbiter.acceptMove}
    t[Darkness][Player] = {moveTo=TileArbiter.acceptMove, action=Player.startActionWithDarkness}

And, clearly, we’ll need a doggone method in Player.

    t[NPC] = {}
    t[NPC][Monster] = {moveTo=TileArbiter.acceptMove}
    t[NPC][Player] = {moveTo=TileArbiter.acceptMove, action=Player.startActionWithNPC}

function Player:startActionWithNPC(anNPC)
    anNPC:actionWithPlayer(self)
end

Let’s run this baby.

squished horn girl

Our Horn Girl is squished. We’d better look at how we handle the Player’s graphics.

Copying from Princess, we implement this ad-hocery:

function NPC:draw(tiny, center)
    if tiny then return end
    pushMatrix()
    translate(center.x, center.y)
    sprite(self.sprite,0,30, 66,112)
    popMatrix()
end

The effect is perfect:

horn girl beside princess

I change the NPC’s TA table to refuseMove so that people and monsters can’t step on the Horn Girl. Now let’s see if we can rig her for a query. We should be able to just have her respond to query:

function NPC:query()
    return "I am your friendly neighborhood Horn Girl!\nAnd do I have a super quest for you!"
end

Sure enough:

query lines come out

Now let’s give her some state, so that she can have more than one message. Nothing fancy, just a few lines.

function NPC:init(tile)
    tile:moveObject(self)
    self.sprite = asset.builtin.Planet_Cute.Character_Horn_Girl
    self.messages = {
    "I am your friendly neighborhood Horn Girl!\nAnd do I have a super quest for you!",
    "I am looking for my lost Amulet of Cat Persuasion,\nso that I can persuade the Cat Girl to let me pet her kitty.",
    "If you can find it and bring it to me,\nI shall repay you generously."}
    self.messageNumber = 1
end

function NPC:query()
    local m = self.messages[self.messageNumber]
    self.messageNumber = self.messageNumber + 1
    if self.messageNumber > #self.messages then
        self.messageNumber = 1
    end
return m
end

three messages

That works nicely. I think we’ll declare this a win and go show the Product Owner our first cut at a quest. And ship it, too.

Commit: First cut NPC.

Let’s sum up.

Summary

So, aside from broken tests, things went well enough. The NPC has gone in as a very simple object, and I think she’ll stay simple, given only some kind of more capable state-driven conversation ability. And we’ll probably want to come up with a dialog window, although it might be that just using ?? will give us most of what we need. We’ll see.

Regarding running the tests, I can’t even explain what happened.

“Human Error”

When we encounter what at first seems to be “human error”, we should think, instead, that it is a system error, an error in the system of our work. We should then see how to improve the system so that kind of human error becomes impossible, or at least less likely.

In a real production system, we’d have a build, and we’d make the build fail if there were broken or ignored tests. That would have stopped me from making a zip file that had errors in it.

In Codea Lua, we have no such capability: there is no build procedure. What else might we do?

One really harsh thing would be to modify CodeaUnit and Main so that if the tests have errors, you can’t click past to run the program anyway. It seems likely that I had been ignoring those tests for a while, just clicking through the red screen. I don’t remember doing that. Another possibility is that I typed in those end of session refactorings and never ran the code, but I can’t imagine that I’d do that.

Checking commits, I see that in fact I did not commit that final refactoring of Loot yesterday, the one that finally took createInventoryItem down to one line. Maybe I was just too happy with the refactoring and wanted to sum up.

I’ll have to think about how to improve my system to avoid today’s unpleasant discovery.

I wonder if I could make CodeaUnit put a file somewhere or some other indication that there is currently a failure. Anyway …

NPC seems a good start.

We do have a little NPC object, just a few methods, and it already knows how to give a series of messages to the Player. We have a decent place to stand, and it seems like it will be fairly straightforward.

All in all, a satisfactory morning. I even got to the Post Office before it rained, so I didn’t have to put the top up.

See you next time! Don’t forget to write and tell me what I should do …


D2.zip