Let’s take another small step toward shunting actions. The plan comes together!!! I am chuffed!
I have in mind a small object, let’s call it an Action, containing a function to be performed, and the argument for that function call. A plan will consist of an array of Action instances.
We have at least one test that may be impacted by this idea, depending on how we go about it.
function Tests:test_first_inversion()
siding5 = Siding(5, "21345")
siding31 = Siding(3)
siding32 = Siding(3)
loco = Locomotive(siding5, siding31, siding32)
plan = loco:plan_one_step()
self:assert_equals(plan, "m1 p1 home m2 p0 home m1 p1 home m2 p2 home m1 p0 home")
end
Maybe we’ll just change plan_one_step
to return a plan_string instead of a plan. And … see me using th notion of a plan as a collection of actions? That suggests that there might be another object, a plan, that acts as that collection. Often instead of using a simple array, our code can be improved by having a specialized collection. We’ll see.
Here’s our current plan-creating method:
function Locomotive:plan_inversion_at_1()
local get_larger = "m1 p1 home"
local cache_it = "m2 p0 home"
local get_smaller = "m1 p1 home"
local attach_larger = "m2 p2 home"
local put_away = "m1 p0 home"
return self:assemble(get_larger, cache_it,
get_smaller, attach_larger, put_away)
end
I’m going to do something a bit tricky, to get to green as quickly as I can: I’m going to keep this method producing strings as it does, but for each of the string segments, I’ll create Actions as well, interleaved like this:
function Locomotive:plan_inversion_at_1()
self.plan = {}
local get_larger = "m1 p1 home"
self.add_action(self.move, 1)
self.add_action(self.pull, 1)
local cache_it = "m2 p0 home"
self.add_action(self.move, 2)
self.add_action(self.pull, 1)
local get_smaller = "m1 p1 home"
self.add_action(self.move, 1)
self.add_action(self.pull, 1)
local attach_larger = "m2 p2 home"
self.add_action(self.move, 1)
self.add_action(self.pull, 2)
local put_away = "m1 p0 home"
self.add_action(self.move, 1)
self.add_action(self.pull, 0)
return self:assemble(get_larger, cache_it,
get_smaller, attach_larger, put_away)
end
I think I can actually make this work without a new class Action, with the right add_action
. I can get to green, of course, with a stubbed add_action
:
function Locomotive:add_action(func, argument)
end
The tests all run, so that code executed without fail. Not too hard but every little step without breaking the tests is a good one. Now for an add_action
that does something, let’s just add tiny tables to the plan
:
function Locomotive:add_action(func, argument)
table.insert(self.plan, {func, argument})
end
So far so good, that should run green also. It does not(!), saying
lua_script:185: attempt to index function with 'plan'
lua_script:185 function add_action
lua_script:166 function plan_inversion_at_1
I’ll bet I have been saying . instead of colon. Sure enough, every single call to add_action was dotted. Grrr. Easily fixed, and the tests are green.
But now we should be able to execute the plan and see it work. I’ll write a new test:
function Tests:test_reverse_two_via_plan()
siding5 = Siding(5, "21345")
siding31 = Siding(3)
siding32 = Siding(3)
loco = Locomotive(siding5, siding31, siding32)
loco:plan_inversion_at_1()
loco:execute_plan()
self:assert_equals(loco:siding_car_string(1), "12345", "sorted!")
self:assert_equals(loco:car_string(), "", "loco", "empty")
end
And we need execute_plan
:
function Locomotive:execute_plan()
for i, pair in self.plan do
pair[1](pair[2])
end
end
Unless I miss my guess, that code will call the function that is the first thing in each pair, passing the second thing as the parameter. If this works, the test will pass. I am hopeful but far from certain. I get an error:
lua_script:136: attempt to index number with 'at'
lua_script:136 function move
lua_script:190 function execute_plan
Huh. Move is this:
function Locomotive:move(siding_number)
self.at = siding_number -- line 136
end
What is that trying to tell me?
function Locomotive:execute_plan()
for i, pair in self.plan do
local f = pair[1]
local a = pair[2]
f(self, a)
end
end
The test executes now, but does not get the right answer:
[10:33] Inglenook 20250515: test_reverse_two_via_plan: 21345 was expected to be 12345 (sorted!)
And I think I see the bug … it is in my hand-compilation of the plan!
function Locomotive:plan_inversion_at_1()
self.plan = {}
local get_larger = "m1 p1 home"
self:add_action(self.move, 1)
self:add_action(self.pull, 1)
local cache_it = "m2 p0 home"
self:add_action(self.move, 2)
self:add_action(self.pull, 0) -- was 1
local get_smaller = "m1 p1 home"
self:add_action(self.move, 1)
self:add_action(self.pull, 1)
local attach_larger = "m2 p2 home"
self:add_action(self.move, 2) -- was 1
self:add_action(self.pull, 2)
local put_away = "m1 p0 home"
self:add_action(self.move, 1)
self:add_action(self.pull, 0)
return self:assemble(get_larger, cache_it,
get_smaller, attach_larger, put_away)
end
And my test runs! Life is good.
It is time for me to go to the main grid, let me summarize what has happened here.
We had two tests of interest, on that produced a text plan, and one that executed the moves from that plan directly, by calling move
and pull
. Both of those correctly invert the first two cars in a train in the 5-siding.
Then we created another kind of plan, a table of function+argument pairs, following the design of the text plan. I made at least two typographic mistakes in that plan, due to too much copying and pasting, most likely. But once the plan was in place, it worked as intended and fixed up the broken train.
The way it works is a bit deep in the bag of tricks, although pretty reasonable in a language with first-class functions—functions that can be saved in variables and passed around just like strings or numbers. We save the function move
itself in the first element of a two-element table, and the desired argument, an integer, in the second, and append the pair table to our plan table.
When it comes time to execute the plan, we fetch the function, and call it. My big mistake was in not passing in self
to the function. Since it is a method of Locomotive, it needs self
in order to access the sidings and cars and such. Then I had made two smaller mistakes in transcribing the plan by hand. Once all that was sorted, the train was sorted as well.
I am pleased: I love it when a plan comes together.
A question that interests me, if not my readers, is whether to keep this table of nested tables, or to have small objects instead. We’ll probably try it just to see what we like best. Objects might be a classier design, but they may also take up more code without offering much real benefit. Because we’re learning how to use SLua, I think we’ll do well to try more than one way of doing things.
See you soon! Safe paths.