Let’s try our new FSM and query logic in the Horn Girl NPC. Watch him add duplication and then reduce it!

The Horn Girl’s Finite State Machine table looks like this:

    {
        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:publishMessage(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 table has four states to help her produce her three initial messages, and two more for her thank you / hello friend state. The messages are oddly indexed by state name. That could have been done in one step, by naming the messages, but for some reason, whoever programmed that didn’t think of it.

We now have a new capability in FSM, which is that if certain callbacks return false, the transition will not occur. That should allow us to have just one state for the initial messages, and one state, or two as we may wish, for thank you and hello friend.

This is worth doing, because in the fullness of time, our game will presumably have many NPCs, and right now, they’re not easy to set up and require custom programming for the message stuff. That means that the Game Design people will need to request development work frequently, as they shape the NPCs. We’d prefer to give them the necessary bits and let them assemble what they need. So we’re working and reworking this first NPC, to discover what is needed.

I think we’ll have three states for our Horn Girl, one where she is unsatisfied, because she doesn’t have the Amulet of Cat Persuasion, one where she receives it and gives the player a reward, and one where she is friendly, and perhaps even helpful.

I’m not locked in on three. It may turn out that the code will tell us that two will suffice, or that four will be better. Certainly if we make her more useful, she’ll need more states. For now, I’m thinking three, maybe two.

To me, part of the joy in programming is discovering that my initial idea isn’t always the best, perhaps almost never my best, and that listening to what the code is saying helps me find better ways to do things. It’s like finding a nice shell on the beach.

OK, Those Messages

I think we should break apart the initial three messages from the other two, as the first three are clearly associated with a different state. And I think I’ll remove the state_to_message table entirely. Things will be broken briefly, but we’re changing how this works.

        data = {
            unsat_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.",
            },
            sat_messages = {
                "Thank you!",
                "Hello, friend!",
            },
        }

Oh, that reminds me. I checked, and every NPC does get a unique table, so that we can safely store variable information in the table if we need to. And we will need to.

Let’s see. We have callbacks on entering a state, and on leaving it. The leaving ones can refuse the leaving. The entering ones do not. So we want to refuse to leave the unsatisfied state until all the messages have been sent.

And, you know? I think after that we want another state, waiting, where we might still say things when queried, but we’re not yet satisfied. We’ll see. We’re shaping this vase on the wheel, to find out what the clay wants to do.

First cut is this:

        initial="unsat",
        events = {
            {event="query", from="unsat", to="wait"},
            {event="received", from={"unsat","wait"}, to="thanks"},
            
            {event="query", from={"ty","ty1"}, to="ty1"},
        },

The first two I feel pretty sure about. I left the white space to signify that I am far less certain about the last one.

Now we need a message display sequence from query.

        callbacks = {
            on_leave_unsat = function(fsm,event,from,to)
                msgs = fsm.data.unsat_messages
                index = fsm.data.unsat_index
                Bus:informDotted(msgs[index])
                index = index + 1
                fsm.data.unsat_index = index
                return index > #msgs
            end,
        },

Looks like I need that index. Let’s say this:

        callbacks = {
            on_leave_unsat = function(fsm,event,from,to)
                msgs = fsm.data.unsat_messages
                index = fsm.data.unsat_index or 1
                Bus:informDotted(msgs[index])
                index = index + 1
                fsm.data.unsat_index = index
                return index > #msgs
            end,
        },

We’ll lazy-init to 1, update thereafter.

Mmrf. Right now I have no test for this other than to try it. I am to impatient to write one, I’m goin in. I expect an explosion but would be happy to have some messages come out.

Tests fail saying:

1: Recognizes cat amulet -- NPC:55: attempt to index a nil value (field 'data')

Ah yes, we need to refer to tab. This is not as convenient as it might be.

        callbacks = {
            on_leave_unsat = function(fsm,event,from,to)
                msgs = fsm.tab.data.unsat_messages
                index = fsm.tab.data.unsat_index or 1
                Bus:informDotted(msgs[index])
                index = index + 1
                fsm.tab.data.unsat_index = index
                return index > #msgs
            end,
        },

A test may fail … and it does:

1: Recognizes cat amulet  -- Actual: I am your friendly neighborhood Horn Girl!
And do I have a super quest for you!, Expected: Thank you!

I think that’s OK, we just didn’t get there yet.

Test in game. Take my word for it, I’ll spare you the megabytes. The three planned messages come out, one for each ??. After that, nothing. That’s consistent with where we are on the table. We should have entered the state wait. Let’s implement it.

        events = {
            {event="query", from="unsat", to="wait"},
            {event="query", from="wait", to="wait"},
            {event="received", from={"unsat","wait"}, to="thanks"},
            
            {event="query", from={"ty","ty1"}, to="ty1"},
        },
        callbacks = {
            on_leave_unsat = function(fsm,event,from,to)
                msgs = fsm.tab.data.unsat_messages
                index = fsm.tab.data.unsat_index or 1
                Bus:informDotted(msgs[index])
                index = index + 1
                fsm.tab.data.unsat_index = index
                return index > #msgs
            end,
            on_leave_wait = function(fsm,event,from,to)
                if event ~= "query" then return true end
                Bus:informDotted("I sure hope you can find the Amulet for me!\nI'll be so grateful!")
            end,
        },

Here’s the sequence so far, as intended:

thru wait state

Now let’s see about what happens when we receive the Amulet.

The table says this:

        events = {
            {event="query", from="unsat", to="wait"},
            {event="query", from="wait", to="wait"},
            {event="received", from={"unsat","wait"}, to="thanks"},
            
            {event="query", from={"ty","ty1"}, to="ty1"},
        },

We generate the “received” message here:

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

The Amulet, when used, publishes “catPersuasion”, and we’re signed up for it:

function NPC:init(tile)
    Bus:subscribe(self, self.catPersuasion, "catPersuasion")
    tile:moveObject(self)
    self.sprite = asset.builtin.Planet_Cute.Character_Horn_Girl
    self.fsm = FSM(self:hornGirlTable())
end

Arguably we’d want to do all that inside the particular FSM table. One thing at a time …

So we’ll get the received event, and we’ll go to state “thanks”. Here I think we’ll want to use an enter method.

            on_enter_thanks = function(fsm,event,from,to)
                Bus:informDotted("Thank you so much!\nPlease accept this useful gift!")
                -- give something
            end,

This movie shows that things are working but also discovers a bug in our FSM:

extra message

I gave the Amulet before the Horn Girl had had a chance to give her whole initial spiel. The transition table allows that. However, the leave message triggered, and then the enter message. We’d like to give the leave message only if the event is “query”.

            on_leave_unsat = function(fsm,event,from,to)
                if event ~= "query" then return end
                msgs = fsm.tab.data.unsat_messages
                index = fsm.tab.data.unsat_index or 1
                Bus:informDotted(msgs[index])
                index = index + 1
                fsm.tab.data.unsat_index = index
                return index > #msgs
            end,

With that fixed, we are safely in the thanks state. We have given the gift, though that is not yet implemented. We can only exit this state on an additional “query” and stay in that final state. Let’s call it satisfied:

        events = {
            {event="query", from="unsat", to="wait"},
            {event="query", from="wait", to="wait"},
            {event="received", from={"unsat","wait"}, to="thanks"},
            {event="query", from="satisfied", to="satisfied"},

And a message:

            on_enter_satisfied = function(fsm,event,from,to)
                Bus:informDotted("Hello, friend!")
            end,

Let’s make a movie of the entire sequence. Oh, darn, I lost an event in a cut-and-paste drive-by.

        events = {
            {event="query", from="unsat", to="wait"},
            {event="query", from="wait", to="wait"},
            {event="received", from={"unsat","wait"}, to="thanks"},
            {event="query", from={"thanks","satisfied"}, to="satisfied"},
        },

horn girl sequence is complete

The sequence is complete. We aren’t giving the Player anything yet, but we didn’t intend to. The event tables are simpler, since now we have more capability than before, and we have found a way to emit multiple messages and stay in one state until they’re all done.

However, we do that at the cost of some callbacks, which are rather customized at this point:

        callbacks = {
            on_leave_unsat = function(fsm,event,from,to)
                if event ~= "query" then return end
                msgs = fsm.tab.data.unsat_messages
                index = fsm.tab.data.unsat_index or 1
                Bus:informDotted(msgs[index])
                index = index + 1
                fsm.tab.data.unsat_index = index
                return index > #msgs
            end,
            on_leave_wait = function(fsm,event,from,to)
                if event ~= "query" then return true end
                Bus:informDotted("I sure hope you can find the Amulet for me!\nI'll be so grateful!")
            end,
            on_enter_thanks = function(fsm,event,from,to)
                Bus:informDotted("Thank you so much!\nPlease accept this useful gift!")
                -- give something
            end,
            on_enter_satisfied = function(fsm,event,from,to)
                Bus:informDotted("Hello, friend!")
            end,
        },

I think these all will turn out to be standard patterns of usage, but they give me some concern.

We have one test that has been failing. Let’s fix that, commit, and then reflect.

1: Recognizes cat amulet  -- Actual: Thank you so much!
Please accept this useful gift!, Expected: Thank you!

The test:

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

Our fake bus defaults to looking for “Thank you!” but we can set it to anything. In this case:

        _:test("Recognizes cat amulet", function()
            local npc = NPC(FakeTile())
            _:expect(Bus.subscriber, "did not subscribe").is(npc)
            Bus:expect("Thank you so much!\nPlease accept this useful gift!")
            npc:catPersuasion()
        end)

Test runs. Commit: NPC Horn Girl’s FSM updated, fewer states needed to run a message sequence.

OK, let’s look upon what we have wrought, and see whether it is good.

Reflect

Here’s her table:

{
        initial="unsat",
        events = {
            {event="query", from="unsat", to="wait"},
            {event="query", from="wait", to="wait"},
            {event="received", from={"unsat","wait"}, to="thanks"},
            {event="query", from={"thanks","satisfied"}, to="satisfied"},
        },
        callbacks = {
            on_leave_unsat = function(fsm,event,from,to)
                if event ~= "query" then return end
                msgs = fsm.tab.data.unsat_messages
                index = fsm.tab.data.unsat_index or 1
                Bus:informDotted(msgs[index])
                index = index + 1
                fsm.tab.data.unsat_index = index
                return index > #msgs
            end,
            on_leave_wait = function(fsm,event,from,to)
                if event ~= "query" then return true end
                Bus:informDotted("I sure hope you can find the Amulet for me!\nI'll be so grateful!")
            end,
            on_enter_thanks = function(fsm,event,from,to)
                Bus:informDotted("Thank you so much!\nPlease accept this useful gift!")
                -- give something
            end,
            on_enter_satisfied = function(fsm,event,from,to)
                Bus:informDotted("Hello, friend!")
            end,
        },
        data = {
            unsat_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.",
            },
            sat_messages = {
                "Thank you!",
                "Hello, friend!",
            },
        }
    }

First thing I notice is that the sat_messages aren’t used. Should remove unused things. But let’s look at the big picture before we improve this, if we choose to.

I notice that we use two different kinds of callbacks, both “enter” ones and “leave” ones. That seemed to be what we needed but it’s sure to be error-prone in practice, because it makes thinking about the transitions more complicated.

Arguably I should be drawing diagrams of the states before typing them in anyway, but still, it would be easier to understand if we didn’t have to use both “enter” and “leave” callbacks in the same simple machine.

Duplication!!!

There is duplication here! It’s not your easy-to-see five lines looking just alike, but it’s there.

We have two callbacks conditioned by which event they want to accept, and which ones reject. Two. That’s duplication. And all four of our events emit exactly one informDotted message per occurrence. Three of them just have one, on goes through some rigmarole and then just says one.

We should find a way to make that easier, and ideally, to make all four cases the same.

I’m going to do another one of those “Watch This!” things.

Watch This!

Also hold my chai.

I first saw something like this done by Kent Beck, decades ago, where he had two methods that looked different to me, and the rest of the class, and he said they were the same, then he made them more the same, and then, now that they were the same, he pulled out the duplication and made the code better, to our amazement.

What I’m about to do isn’t quite that amazing, but I think we’ll like it.

My plan is this: I’m going to convert each of those callbacks to do the same thing, namely display one message from a table, and return false if there are more entries in the table to display. Yes, all three of those will have just a one-entry table, so they will always return on the first go. But once they are the same, I think we’ll see something interesting to do.

It might be that we see it’s interesting to revert. But I don’t think so.

Here goes. First I set up the message tables:

        data = {
            unsat_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.",
            },
            wait_messages = {
                "I sure hope you can find the Amulet for me!\nI'll be so grateful!",
            },
            thanks_messages = {
                "Thank you so much!\nPlease accept this useful gift!",
            },
            satisfied_messages = {
                "Hello, friend!",
            },

Then I use them in the callbacks:

Eeek! In doing that, I notice that the first callback wasn’t using locals. I’ll fix that too.

        callbacks = {
            on_leave_unsat = function(fsm,event,from,to)
                if event ~= "query" then return end
                local msgs = fsm.tab.data.unsat_messages
                local index = fsm.tab.data.unsat_index or 1
                Bus:informDotted(msgs[index])
                index = index + 1
                fsm.tab.data.unsat_index = index
                return index > #msgs
            end,
            on_leave_wait = function(fsm,event,from,to)
                if event ~= "query" then return true end
                local msgs = fsm.tab.data.wait_messages
                Bus:informDotted(msgs[1])
            end,
            on_enter_thanks = function(fsm,event,from,to)
                local msgs = fsm.tab.data.thanks_messages
                Bus:informDotted(msgs[1])
                -- give something
            end,
            on_enter_satisfied = function(fsm,event,from,to)
                local msgs = fsm.tab.data.satisfied_messages
                Bus:informDotted(msgs[1])
            end,
        },

This is a pure refactoring, typos notwithstanding, and works as before. But let’s make more duplication, just to be weird.

        callbacks = {
            on_leave_unsat = function(fsm,event,from,to)
                if event ~= "query" then return end
                local msgs = fsm.tab.data.unsat_messages
                local index = fsm.tab.data.unsat_index or 1
                Bus:informDotted(msgs[index])
                index = index + 1
                fsm.tab.data.unsat_index = index
                return index > #msgs
            end,
            on_leave_wait = function(fsm,event,from,to)
                if event ~= "query" then return true end
                local msgs = fsm.tab.data.wait_messages
                local index = fsm.tab.data.wait_index or 1
                Bus:informDotted(msgs[index])
                index = index + 1
                fsm.tab.data.wait_index = index
                return index > #msgs
            end,
            on_enter_thanks = function(fsm,event,from,to)
                local msgs = fsm.tab.data.thanks_messages
                local index = fsm.tab.data.thanks_index or 1
                Bus:informDotted(msgs[index])
                index = index + 1
                fsm.tab.data.thanks_index = index
                return index > #msgs
                -- give something
            end,
            on_enter_satisfied = function(fsm,event,from,to)
                local msgs = fsm.tab.data.satisfied_messages
                local index = fsm.tab.data.satisfied_index or 1
                Bus:informDotted(msgs[index])
                index = index + 1
                fsm.tab.data.satisfied_index = index
                return index > #msgs
            end,
        },

Wow, there’s a lot of duplication up in this baby.

OK, clever boy, that doesn’t quite work. The reason is that the on_enter methods don’t honor the flag. You’re in the state now, And that means that our code will run off the end of the table.

However, we can fix that with more duplication:

            on_enter_satisfied = function(fsm,event,from,to)
                local msgs = fsm.tab.data.satisfied_messages
                local index = fsm.tab.data.satisfied_index or 1
                if index > #msgs then index = 1 end
                Bus:informDotted(msgs[index])
                index = index + 1
                fsm.tab.data.satisfied_index = index
                return index > #msgs
            end,

And the other three, of course.

Yes, we are making this code worse. But we’re doing it to discover commonalities that we can exploit. This should now work adequately.

Yes, the behavior is as intended. I think, however, that it’s more useful to set the index to the last table element than the first. That way, we can build a table of messages such that the last one just repeats. We might even be able to reduce our event table if we do that.

Little jolt of joy there. We discovered something interesting and unforeseen. Neat.

This change:

            on_enter_satisfied = function(fsm,event,from,to)
                local msgs = fsm.tab.data.satisfied_messages
                local index = math.min(fsm.tab.data.satisfied_index or 1, #msgs)
                Bus:informDotted(msgs[index])
                index = index + 1
                fsm.tab.data.satisfied_index = index
                return index > #msgs
            end,

The math.min does the job for us just fine.

In my first cut at that I failed to change the names inside, used “unsat”. The trials of cut and paste.

OK, let’s see if we can pull out the common lines now. Well, not quite, they all refer to different table elements. Let’s fix that. I’ll try it once first, as I’m sure to get it wrong.

And I do. This works:

            on_leave_unsat = function(fsm,event,from,to)
                if event ~= "query" then return end
                local element = "unsat"
                local msg_name = element.."_messages"
                local idx_name = element.."_index"
                local msgs = fsm.tab.data[msg_name]
                local index = math.min(fsm.tab.data[idx_name] or 1, #msgs)
                Bus:informDotted(msgs[index])
                index = index + 1
                fsm.tab.data[idx_name] = index
                return index > #msgs
            end,

It’s a bit ugly (A bit?) Let’s improve somewhat:

            on_leave_unsat = function(fsm,event,from,to)
                if event ~= "query" then return end
                local element = "unsat"
                local data = fsm.tab.data
                local msg_name = element.."_messages"
                local idx_name = element.."_index"
                local msgs = data[msg_name]
                local index = math.min(data[idx_name] or 1, #msgs)
                Bus:informDotted(msgs[index])
                index = index + 1
                data[idx_name] = index
                return index > #msgs
            end,

That works. Now we can increase the duplication even more:

Everything still works. Let’s commit: Added massive duplication to callbacks for NPC Horn Girl.

Now then. We can extract this duplication. But as what? A function? A method? If so, where?

Two obvious choices, method on NPC, method on FSM. The FSM is very highly-focused on being a Finite State Machine, so let’s honor that and put our new stuff into NPC. That’s accessible by self. (I predict we won’t be satisfied with this decision, but that the dissatisfaction won’t show up today.)

Well, that was the fastest mistake of the day. Our code references the FSM a lot and self not at all. That argues for adding it to FSM. I’m going to stick to my original decision for now, because this is still pretty nascent.

            on_leave_unsat = function(fsm,event,from,to)
                if event ~= "query" then return end
                local element = "unsat"
                return self:publishMessages(element, fsm,event,from,to)
            end,
            on_leave_wait = function(fsm,event,from,to)
                if event ~= "query" then return true end
                local element = "wait"
                return self:publishMessages(element, fsm,event,from,to)
            end,
            on_enter_thanks = function(fsm,event,from,to)
                local element = "thanks"
                return self:publishMessages(element, fsm,event,from,to)
                -- give something
            end,
            on_enter_satisfied = function(fsm,event,from,to)
                local element = "satisfied"
                self:publishMessages(element, fsm,event,from,to)
            end,

function NPC:publishMessages(element, fsm,event,from,to)
    local data = fsm.tab.data
    local msg_name = element.."_messages"
    local idx_name = element.."_index"
    local msgs = data[msg_name]
    local index = math.min(data[idx_name] or 1, #msgs)
    Bus:informDotted(msgs[index])
    index = index + 1
    data[idx_name] = index
    return index > #msgs
end

How about that? This works as intended.

Commit: Refactor removing duplication from Horn Girl’s callbacks.

Let’s look at this and see how to improve it even more. And to marvel at how creating more duplication has resulted in far less. We’ll also see some opportunities arising, I think.

Here’s the whole Horn Girl table:

{
        initial="unsat",
        events = {
            {event="query", from="unsat", to="wait"},
            {event="query", from="wait", to="wait"},
            {event="received", from={"unsat","wait"}, to="thanks"},
            {event="query", from={"thanks","satisfied"}, to="satisfied"},
        },
        callbacks = {
            on_leave_unsat = function(fsm,event,from,to)
                if event ~= "query" then return end
                local element = "unsat"
                return self:publishMessages(element, fsm,event,from,to)
            end,
            on_leave_wait = function(fsm,event,from,to)
                if event ~= "query" then return true end
                local element = "wait"
                return self:publishMessages(element, fsm,event,from,to)
            end,
            on_enter_thanks = function(fsm,event,from,to)
                local element = "thanks"
                return self:publishMessages(element, fsm,event,from,to)
                -- give something
            end,
            on_enter_satisfied = function(fsm,event,from,to)
                local element = "satisfied"
                self:publishMessages(element, fsm,event,from,to)
            end,
        },
        data = {
            unsat_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.",
            },
            wait_messages = {
                "I sure hope you can find the Amulet for me!\nI'll be so grateful!",
            },
            thanks_messages = {
                "Thank you so much!\nPlease accept this useful gift!",
            },
            satisfied_messages = {
                "Hello, friend!",
            },
        }
    }

Looking at those element decisions, at first they appear to be different, in that the first two are equal to the from of their transitions, and the second are equal to the to of theirs.

Another way of looking at it, however, is that they are all equal to the state of interest, i.e. the from for the leaving ones and the to for the arriving ones.

What if the FSM had another parameter in its calling sequence, naming the “state of interest”? We could use that as our “element” and remove the need for the custom parameter.

Or maybe we can embed the information another way.

If we knew the name of the callback, we could break the word off the end and use it.

I was thinking maybe we could embed extra information in the event table entries, but one difficulty with that is that we can’t see those. But what if we allow one more field, say arg, that gets passed to the event, from whatever entry we’re using. Is that possible? We have to review FSM to be sure.

function FSM:doCallback(callbackName, event, trans, args)
    local cb = self.tab.callbacks
    if cb[callbackName] then
        return cb[callbackName](self, event, trans.from, trans.to, table.unpack(args))
    else
        return true
    end
end

I see no reason why we can’t do this:

        return cb[callbackName](self, event, trans.from, trans.to, trans.arg, table.unpack(args))

Now we probably break some tests if we just pass that inevitable nil in, but that’s OK. Our callbacks all need to accept one more arg at least. One test did break, and adding arg to its expected list did the job.

Here’s what we have now:

{
        initial="unsat",
        events = {
            {event="query", from="unsat", to="wait", arg="unsat"},
            {event="query", from="wait", to="wait",arg="wait"},
            {event="received", from={"unsat","wait"}, to="thanks", arg="thanks"},
            {event="query", from="thanks", to="satisfied", arg="thanks"},
            {event="query", from="satisfied", to="satisfied", arg="satisfied"},
        },
        callbacks = {
            on_leave_unsat = function(fsm,event,from,to, arg)
                if event ~= "query" then return end
                return self:publishMessages(arg, fsm,event,from,to)
            end,
            on_leave_wait = function(fsm,event,from,to, arg)
                if event ~= "query" then return true end
                return self:publishMessages(arg, fsm,event,from,to)
            end,
            on_enter_thanks = function(fsm,event,from,to, arg)
                return self:publishMessages(arg, fsm,event,from,to)
                -- give something
            end,
            on_enter_satisfied = function(fsm,event,from,to, arg)
                self:publishMessages(arg, fsm,event,from,to)
            end,
        },
        data = {
            unsat_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.",
            },
            wait_messages = {
                "I sure hope you can find the Amulet for me!\nI'll be so grateful!",
            },
            thanks_messages = {
                "Thank you so much!\nPlease accept this useful gift!",
            },
            satisfied_messages = {
                "Hello, friend!",
            },
        }
    }

With this little change in FSM, to pass in the arg field of the operative transition, we have our parameter represented in data, not in code.

Commit: FSM passes arg element of transition to relevant callbacks. Used as lookup parameter or any other purpose. NPC uses for table identification in publishMessages.

This has been a lot. We need to let it cool a bit, so let’s sum up.

Summary

We set out to simplify the NPC Finite State Machine, by allowing a single state to publish multiple messages before allowing transition to a new state. To do that, we used yesterday’s feature of returning false from a callback to say “don’t transition after all”.

Having done that, we worked through our new transition table to call Bus:informDotted for all the messages we had.

Then we observed that our four callbacks were somewhat similar, though one was much large and more complicated than the other three.

In what may have seemed madness, we made the simple three more complicated, to look more like the one. And we kept modifying them all to make them more and more similar.

In the end we had all the callbacks doing the same thing, iterating over a table of one or more messages, sticking on the last message if a transition didn’t occur, each callback looking up its own messages and counter variable using, first, the name we had given to the four message tables.

Those tables happened to be named according to a pattern, by the name of the callback that needed them, which was, in turn, the name of a from or to of some transition.

We plugged in a literal in each one of the four, which we used to look up our messages and counter. That allowed us to extract a method and pass it the literal for each of our four cases. That removed all the duplication we had created.

Then we added a parameter, arg to transition tables, and passed it, by convention, as a fifth argument to any callback. Setting that arg to the literal needed, we removed the literal from the code, and turned it into data.

And we did it in small sensible steps, and typos aside, we never broke anything.

If that isn’t fun, I don’t know what is.

What lessons might we draw? Well, you can choose your own but I am thinking about these:

  • Sometimes increasing duplication leads to a way to reduce multiple different things into one kind of thing;
  • Converting procedure to data is very often helpful;
  • Iterating of a design, looking for bumps and knots often leads to a better, more compact, more usable design;

And one more:

  • I bet we’re not done yet

A joyful morning for me, and I hope for you! See you next time!


D2.zip