Those words came up in our Zoom Ensemble last night, and they’re part of my theme this morning.

The Zoom at that point held GeePaw Hill, Chet Hendrickson, and me. It was GeePaw who said something to the effect that we three there took more joy in the pure craft of programming than many programmers we know, and that he wished we could better share the joy.

Parts of this are true.

I wrote a card as I sat in the dark of the Zoom Room, and after looking at it for a while this morning wondering why I had written “Shame the dog”, since we don’t even have a dog, I realized I had written “Share the joy”.

Whatever your job, you spend many hours doing it. If you’re a programmer, you spend many hours programming. And we do say “spend”. Those hours are too often a drab lost period of the best hours of the day, coding away to earn a crust of bread and maybe a Corvette or a college education for the kids.

I don’t know how one can find joy if one is an accountant, or a mid-level manager, or even a vice president. I’ve held two of those jobs, for a long time, and I still don’t know. And I don’t know enough about accounting to be able to guess where the joy is in that.

Programming, I do know.

And that’s what I’m always writing about here, but I feel that I can make myself more clear, and I’m dedicating myself to trying to do that.

Programming This Way is FUN!

I’ve written a lot about Dark Scrum and how the Increment of Software is the best defense I know against the all-too-common misunderstanding of Scrum and other methods, leading to misuse, and mistreatment of developers. And that remains true:

The techniques I write about (interminably it seems) are the fastest way I know to write software over the medium and long term, and the best way to have something new to show every few days, and the best way I know to be positioned to work on whatever the priority of tomorrow turns out to be.

And I write, often, about the mistakes I make, and I try to be clear that often I do feel badly about making them, and to be clear about how I try to figure out what caused the mistake, and how I might avoid that kind of thing in the future.

I even try to be clear that while I do feel badly, I don’t blame myself, I see mistakes that escape as flaws in the system of my work, not deep flaws in my person.

What I don’t do as often as I might is Share the Joy.

I Do This Every Day - Why?

This is article 216 about this single dungeon program! there are 66 Asteroids articles, 77 Space Invaders articles, 30 or more Spacewar articles.

I write more than five programming articles in the average week. Why do you imagine that I do it? It sure isn’t for the occasional Ko-Fi that someone springs for.

I do this because the 2, 3, 5 hours when I’m writing these programs and articles are immense fun. I’m learning, I’m laughing ruefully at how someone who has been programming for six decades can still make ridiculous mistakes, and mostly I’m just smiling at how a program can take shape under my hands, how the code that isn’t as good as it might be can be shaped, smoothed, and reorganized to become better.

I do this because I enjoy watching myself produce code, write bugs, find problems, and I enjoy watching myself do it rather well, as often I manage. And I really enjoy watching myself make an imperfect thing better.

And wow, do I enjoy learning a new lesson. (I even enjoy relearning an old lesson. It’s like greeting an old friend.)

So my point, and I do have one, is that I’m going to try harder to …

Share the Joy

Let’s get to it. I have a fraction of an idea and I think we should work on it. Let’s look into history back …

We wanted to build our first Non-Player Character, the Horn Girl, who would tell a story of a lost Amulet of Cat Persuasion, which she needs so as to be able to pet her friend’s kitty. When the Player finds the Amulet and then finds the Horn Girl and uses the Amulet, she’s giving it to Horn Girl, and Horn Girl is to thank the Player and give them something of value.

We are part-way along that story, with pieces of the flow working.

I looked into ways of handling the flow and settled on the notion of a Finite State Machine (FSM) that would have various states representing the Horn Girl’s state, waiting for the Amulet, getting the Amulet, and having the Amulet.

I researched FSMs, and found a design that I liked, and, because I’m here to enjoy programming, I coded the thing up rather than trying to integrate some other style of programming into mine. It may have taken longer to do that, but my overall experience with adopting today’s wonderful useful framework has been that they never quite match our needs and that learning to bend them to our will isn’t fun.

In a business situation, there are plenty of libraries and frameworks that we’ll be wise to use, but be that as it may, I coded up ours, following the specs of the ones I found.

And I like it–almost.

The Horn Girl uses the FSM, and this is her table as of today:

{
        initial="m0",
        events = {
            {event="query", from="m0", to="m1"},
            {event="query", from="m1", to="m2"},
            {event="query", from="m2", to="m3"},
            {event="query", from="m3", to="m1"},
            {event="received", from={"m0","m1","m2","m3"}, to="ty"},
            {event="query", from={"ty","ty1"}, to="ty1"},
        },
        callbacks = {
            on_enter_state = function(fsm,event,from,to)
                self:setMessage(to)
            end,
        },
        data = {
            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.",
                "Thank you!",
                "Hello, friend!",
            },
            stateToMessage = {m1=1, m2=2, m3=3, ty=4, ty1=5},
        }
    }

This FSM pretty much does what we want, but I don’t entirely love it. Because she has three messages to speak before the quest is fully defined, she has those four “m” states, each one causing her to say one line. Then she enters the thank-you state, “ty”, where she first says “Thank you!” and thereafter just says “Hello, friend”.

We aren’t done yet with her basic behavior, and we can imagine that she should have even more behavior. Since now she sees the Player as a friend, perhaps we should encounter her later on and she might give us hints or help out in other ways. So we might extend this FSM, or create new ones for her subsequent behaviors.

It’s good, and it works nicely. And there are things not to like.

First of all, I think we don’t love the need to have a new state for every message she can speak. I think what I’d like a lot more–what would bring me a spark of joy–would be some kind of “smart state” where she’d stay in that state until her script was used up, and only then go into another state.

When we look at the machine above, we can see that I’ve used the state names to represent the idea in my head, that she has a message mode and then another thank you mode. The entry names don’t do a great job of expressing my thoughts, but they’re good enough. But what would really be nice would be if we could have a top level machine that went from “waiting” to “receiving” to “friend”, and so on, and then somehow within that machine, there’d be smaller machines to spool out the messages or whatever.

I referred in previous articles about sub-states, pushdown automata and GOAP, and it’s tempting to divert and build up a real goal-oriented kind of NPC. Perhaps someday we’ll do that, but I prefer to work more incrementally.

The reason is that working incrementally, I can deliver bits of working function at the surface of the application, while building the infrastructure up underneath. I don’t have to take three weeks off to write a bunch of GOAP articles. This goes to my inherent fear of not delivering features, or, to put a better face on it, my inherent love of always delivering features.

So what we’re going to do is to try to find ways to make it easier to give our NPC behavior by enhancing our tools, not by getting some new tool and trying to plug it in. We may wind up replacing our tool, but if we do, it’ll be more bit-by-bit, while a continuing flow of game features comes out.

Where’s This Joy You Were Speaking Of?

Patience, Prudence, we’ll get there. I need to share my thoughts, so that I’ll understand them, and you’ll at least have a clue what I’m up to.

We’re almost there.

Yesterday, we extended the FSM a bit, so that a callback on an attempt to leave a state can return false and suppress the state transition. My thinking was that we’d use that to write a callback that would keep us in the “m” state until we’d said our piece, and only then could we move to some other state.

We might do that today. But unfortunately, I had another idea.

Imagine a kind of FSM that was custom-made to step through a series of messages, and that when we stepped it, we’d have it return false until it ran out of messages. Then we’d use that FSM inside our top-level one.

One more thing …

The way our current FSM works on query is a bit odd:

        callbacks = {
            on_enter_state = function(fsm,event,from,to)
                self:setMessage(to)
            end,
        },

The issue is that the query function, which all objects respond to, returns a message to be displayed:

function Player:query()
    for i,m in ipairs(self:getTile():nearestContentsQueries()) do
        self:inform(m)
    end
end

function Player:inform(message)
    Bus:inform(message)
end

function Dungeon:nearestContentsQueries(tile)
    local msgs = {}
    local neighbors = self:neighbors(tile)
    for i,tile in ipairs(neighbors) do
        tile:addQueriesToTable(msgs)
    end
    return msgs
end

function Tile:addQueriesToTable(msgs)
    for k,obj in pairs(self:getContents()) do
        if obj.query then
            if #msgs > 0 then
                table.insert(msgs,"...")
            end
            table.insert(msgs,obj:query())
        end
    end
end

If that seems convoluted to you, you’re quite right. I’m not sure how it got that way. We can see, though darkly, that what happens is that we accumulate all the messages from all the objects in all the player-neighboring tiles, interspersing “…” between the batches, and then, finally, we call Bus:inform with all of them.

There’s another issue with this setup: suppose that some dungeon object or entity wants to take an action rather than return a set of messages. It can do that, but it still has to return something to the message table, at least an empty table.

Are We Having Fun Yet?

Hell, yes, we are! We’ve got a fun little puzzle here. There’s this odd, not quite nice setMessage thing in our NPC, which is just there to allow our callback to save away a message for query to print.

It’s no big deal, but it’s a bit of a bump, and when we dig into it, we find that the reason for it all is hidden in the darkness of time. And, for sure, the query code violates the principle of “Tell, don’t ask”. We ask all the neighbors for their query messages and wrap them up in a package and return them to the Player, who then publishes them. Why don’t we tell the neighbors “do your query thing” and let them publish whatever they care to?

That would be better. There is one tiny glitch: those “…” that are supposed to come out ahead of the messages if you’re not the first to respond to query.

So, in aid of making our NPC’s FSM easier, and future message-oriented objects easier, we’re going to untangle this query function, with a major new rule:

When you are sent query, if you have something to say, you push it to the Bus yourself rather than return it.

This is a fairly large refactoring. There are at least ten implementors of query and we need to be sure to change them all. And we have that issue with the “…” as well.

Hm. What if Bus had a new inform kind of method, informDotted, that prepends “…” to the message list? Then we’d have just one issue: we’d get an extra “…” before the first message. Act of faith: we’ll see an easy way to fix this once we get this code clean.

Let’s look at Bus:inform:

function EventBus:inform(message)
    if type(message) == "string" then
        message = splitStringToTable(message)
    end
    for i,msg in ipairs(message) do
        Bus:publish("addTextToCrawl", self, {text=msg})
    end
end

Looks like Bus:informDotted should be easy enough:

function EventBus:informDotted(message)
    Bus:publish("addTextToCrawl", self, {text="..."})
    self:inform(message)
end

I’m not even going to test that. It can’t possibly not work. (See if I’m wrong …(Well, I first wrote Bus: …))

Commit: added `informDotted to EventBus, to display “…” before the messages.

Now, I think what I’ll do is go through the query implementors one by one, changing them to use informDotted, and to return an empty table from query. That way we can convert to the new scheme one at a time. It’ll change the order of the messages, but no one knows what that order should be anyway.

function Loot:query()
    return self.message
end

That becomes:

function Loot:query()
    Bus:informDotted(self.message)
    return {}
end

Test. Works:

potion of monstrous speed

Commit: Loot uses Bus:informDotted.

Clearly, I can do these one at a time, harmlessly, until done. All the other queries work as before, using whatever non-empty table they return. I’m going to do a few at a time now. Here are the changed versions. I hope they’re self-explanatory, and if I encounter one needing explanation, I’ll provide one.

function NullQueryObject:query()
    return {}
end

function Monster:query()
    Bus:informDotted(self.behavior.query(self))
    return {}
end

The Monster one is interesting. It forwards the message off to behavior. On the face of it, we don’t care how that works, but it does mean that we’ll need to come back here to change this when we change the behavior ones. A bit tricky, makes it more fun.

Carrying on …

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

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

I was just thinking of a mistake I could make here. If I were to typo informDotted, we’d have an object who didn’t respond correctly to query, and querying it would crash us.

What can we do about that other than be careful?

Well, we can double-check. I’ve made a card to remind me, but at this moment I don’t have another idea.

Maybe we could write a test to send query to everything and see what it does? Or a test to inspect the code?

Interesting problems!

I’ve paused. Let’s test a bit and then commit.

Do you see the typo in Key? I typed () instead of {}. Tells me that typos are easy, and I know there aren’t useful tests for this. Scary. It’s making me nervous. My joy is reducing.

Commit: NullQueryObject, Monster, WayDown, and Key now use Bus:informDotted on query.

I did test, in a desultory fashion. Very risky. Don’t do like that.

When I think I’m done, I’m going to do some searching. Until then, my nervousness will increase.

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

function Spikes:query()
    Bus:informDotted("Deadly Spikes do damage to tender feet when down and even more when up!")
    return {}
end

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

function Lever:query()
    Bus:informDotted("At first glance, this appears to be a lever.\nDo you think it does anything?")
    return {}
end

This one’s going to break a test:

        _: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)

Test.

6: Decor query text -- Decor:159: attempt to index a nil value (global 'Bus')

Let’s see. We could use our fake Bus and check the message that way, or we could write a private method to get the text and call it from Decor:query and the test. I think I prefer that.

function Decor:queryMessagesPrivate()
    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

function Decor:query()
    Bus:informDotted(self:queryMessagesPrivate())
    return {}
end

And …

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

Test runs!

Joy

A nice solution to an issue where a test wants to see inside what’s going on, without using a fake object. We could certainly do that as well, and save the private method. Not a perfect solution, but since it preserves a test, I like it.

Total joy? No, but a little bit: made it work! Can move on.

We are down to the NPC’s query:

function NPC:query()
    local worked = self.fsm:event("query")
    if worked then
        return self.msg or ""
    else
        return ""
    end
end

The call to the FSM returns false if it didn’t work. At present it will always “work”, because our callback never returns false. Let’s push the publish all the way into the callback.

This is a bit tricky. Better commit those others first! Joy! I almost forgot but I didn’t! Commit: Decor, Spikes, Chest, and Lever use Bus:informDotted for query.

I’m not used to telling you ever time I get a little frisson of pleasure at something working. I suppose I’m worried that I’ll feel like a fool enjoying the little things.

But that’s the point: learning to enjoy the little things, and noticing that we enjoy them.

Now then, NPC:

        callbacks = {
            on_enter_state = function(fsm,event,from,to)
                self:setMessage(to)
            end,
        },

function NPC:setMessage(state)
    local data = self.fsm.tab.data
    local msgIndex = data.stateToMessage[state]
    self.msg = data.messages[msgIndex]
end

function NPC:query()
    local worked = self.fsm:event("query")
    if worked then
        return self.msg or ""
    else
        return ""
    end
end

The query can just fire the event and return {}:

function NPC:query()
    self.fsm:event("query")
    return {}
end

The callback should call publishMessage, not setMessage, for clarity. Rename:

function NPC:publishMessage(state)
    local data = self.fsm.tab.data
    local msgIndex = data.stateToMessage[state]
    Bus:informDotted(data.messages[msgIndex])
end

        callbacks = {
            on_enter_state = function(fsm,event,from,to)
                self:publishMessage(to)
            end,
        },

Let’s test that:

First issue is a couple of tests fail:

1: Recognizes cat amulet -- NPC:99: attempt to call a nil value (method 'informDotted')
2: transitions and messages -- NPC:99: attempt to call a nil value (method 'informDotted')

That’s why we have them. Let’s see what’s up:

        _:test("Recognizes cat amulet", function()
            local npc = NPC(FakeTile())
            _:expect(Bus.subscriber, "did not subscribe").is(npc)
            npc:catPersuasion()
        end)
        
        _:test("transitions and messages", function()
            local msg
            local npc = NPC(FakeTile())
            local fsm = npc.fsm
            msg = npc:query()
            _:expect(msg).is(npc.fsm.tab.data.messages[1])
        end)

We have a FakeBus in use here:

FakeEventBus = class()

function FakeEventBus:subscribe(object, method, event)
    self.subscriber = object
    _:expect(method).is(NPC.catPersuasion)
end

function FakeEventBus:inform(message)
    _:expect(message).is("Thank you!")
end

We can give it informDotted:

function FakeEventBus:informDotted(message)
    return self:inform(message)
end

I expect the tests to run now. But no. What has happened is that now we’re getting lots of things published to our fake bus and we get these errors:

1: Recognizes cat amulet  -- Actual: nil, Expected: Thank you!
2: transitions and messages  -- Actual: I am your friendly neighborhood Horn Girl!
And do I have a super quest for you!, Expected: Thank you!
2: transitions and messages  -- Actual: table: 0x2800f0a00, Expected: I am your friendly neighborhood Horn Girl!
And do I have a super quest for you!

Those messages used to be returned from query, not published so our Fake Bus never saw them. Now it does.

Let’s enhance the fake. We’ll allow it to look for different messages at different times. This will actually make for a better test.

Oh I like this idea … if it works.

function FakeEventBus:subscribe(object, method, event)
    self.checkMessage = "Thank you!"
    self.subscriber = object
    _:expect(method).is(NPC.catPersuasion)
end

function FakeEventBus:expect(msg)
    self.checkMessage = msg
end

function FakeEventBus:inform(message)
    _:expect(message).is(self.checkMessage)
end

Now we can judiciously adjust the expectations,

Full disclosure: I’m a bit nervous about this because of that nil up above. I’m wondering what that is. We’ll find out and when we’ve cracked it, we’ll be happy.

        _:test("transitions and messages", function()
            local msg
            local npc = NPC(FakeTile())
            local fsm = npc.fsm
            Bus:expect("I am your friendly neighborhood Horn Girl!\nAnd do I have a super quest for you!")
            msg = npc:query()
        end)

I remove the check and allow the Fake Bus to check. The #2 test runs. I’ll change the expect to be sure it fails on wrong messages. It does:

2: transitions and messages  -- Actual: I am your friendly neighborhood Horn Girl!
And do I have a super quest for you!, Expected: xxI am your friendly neighborhood Horn Girl!
And do I have a super quest for you!

This test is still failing:

        _:test("Recognizes cat amulet", function()
            local npc = NPC(FakeTile())
            _:expect(Bus.subscriber, "did not subscribe").is(npc)
            npc:catPersuasion()
        end)

The message is:

1: Recognizes cat amulet  -- Actual: nil, Expected: Thank you!

Apparently catPersuasion is causing a publish, And of course it is:

function NPC:catPersuasion()
    self.fsm:event("received")
    Bus:inform(self.msg) -- set via callback
end

Now we don’t need to do that at all, because we’ve changed from set to Publish. I think if we remove that Bus:inform, the test will run, because it starts out expecting “Thank you!”.

function NPC:catPersuasion()
    self.fsm:event("received")
end

Tests run! This makes me happy! I’m nearly ready to commit, but first we’d better run the Horn Girl through her paces.

Horn Girl does her thing

Perfect. Commit: Horn Girl publishes messages using Bus:informDotted. All objects converted, subject to double check.

Double Checking

I am pretty psyched about this, it has gone well. I do want to do two more things:

First, I want to be sure that all the query operations are converted to do their own publishing

Second, I want to be sure that all the calls to informDotted actually say informDotted and not infrmDotted` or something.

After that, I’ll be sure that this change is done.

Then, I’d like to get rid of that first … that comes out, when there’s only one queried object, and I’d like to change the overall query flow to Tell Don’t Ask.

First the checks.

I carefully convince myself that all is well. Now, if I choose to do so, I can unwind the query logic to assume that if an object wants to say something, it just says it.

I’m on a fresh commit so if this goes badly, I’ll be fine.

(This makes me happy, to know that I’m one revert away from a good point,)

Now we change:

function Player:query()
    for i,m in ipairs(self:getTile():nearestContentsQueries()) do
        self:inform(m)
    end
end

No point in us looping over this: nothing should be coming back. We’ll just call it:

function Player:query()
    self:getTile():nearestContentsQueries()
end

Everything should still work, and by golly I’m going to test a bit.

Looks good. Let’s commit: Player no longer informs query messages. None are provided anyway.

Now:

function Tile:nearestContentsQueries()
    return self.runner:getDungeon():nearestContentsQueries(self)
end

Nothing to return here. We could leave it, but let’s remove the return as it makes it more obvious that we’re not returning anything of interest:

function Tile:nearestContentsQueries()
    self.runner:getDungeon():nearestContentsQueries(self)
end

Now …

function Dungeon:nearestContentsQueries(tile)
    local msgs = {}
    local neighbors = self:neighbors(tile)
    for i,tile in ipairs(neighbors) do
        tile:addQueriesToTable(msgs)
    end
    return msgs
end

Ah this gets seriously simpler, but we’ll have to change the Tile message as well:

function Dungeon:nearestContentsQueries(tile)
    for i,tile in ipairs(self:neighbors(tile)) do
        tile:doQueries()
    end
end

We’ll rename the addQueriesToTable and rework:

function Tile:addQueriesToTable(msgs)
    for k,obj in pairs(self:getContents()) do
        if obj.query then
            if #msgs > 0 then
                table.insert(msgs,"...")
            end
            table.insert(msgs,obj:query())
        end
    end
end

That gets tons simpler:

function Tile:doQueries()
    for k,obj in pairs(self:getContents()) do
        if obj.query then
            obj:query()
        end
    end
end

I think that’s solid. Test. All seems good:

test a few

Now we can commit: Player query is now tell-don’t-ask

Sweet. I am smiling just a bit over here.

Now we can get rid of all those return {} things.

function NullQueryObject:query()
end

function Monster:query()
    Bus:informDotted(self.behavior.query(self))
end

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

function Loot:query()
    Bus:informDotted(self.message)
end

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

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

function Spikes:query()
    Bus:informDotted("Deadly Spikes do damage to tender feet when down and even more when up!")
end

function Decor:query()
    Bus:informDotted(self:queryMessagesPrivate())
end

function Lever:query()
    Bus:informDotted("At first glance, this appears to be a lever.\nDo you think it does anything?")
end

function NPC:query()
    self.fsm:event("query")
end

Test. All seems well. Commit: remove unneeded return {} from all query methods.

Nice

In the space of three hours with time out for some munchies and producing so far 1900 words of article or thereabouts, we’ve changed the query support from Ask, to Tell Don’t Ask, and set ourselves up for further improvements.

Over the course of the changes, we committed 8 times and could have done 15 or 20 smaller commits than the ones we did, and every one of those versions was shippable.

That gives me joy. You know why? Because it tells me that I can improve the system as I need to, in just a few minutes at a time, resulting in a larger-scale improvement. That makes it easier to be comfortable with focusing most of my work on features, trusting that I’ll be able to improve the design when I need to.

That takes a bit of skill, and I enjoy exercising that skill, and finding new ways to do things in small bites.

It has been a good morning. I wish you joy … and I am here to tell you that you can find it at work, with your code and your colleagues. Colleagues are better, but the code has joy in it as well, if we look for it.

See you next time!


D2.zip