Dungeon 217
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:
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:
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"},
},
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!