JanetRossini.github.io

Lua, LSL, Blender, Python in Second Life


Project maintained by JanetRossini

More Shunting Actions

May 15, 2025 • [designluaobjectstesting]


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.

Note
You’ll see below that I start with something even simpler, a two-element table with function and argument.

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.

Summing Up

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.

Note
I had intended to create a small Action object, and as a smaller step, just made a simple two-element table.

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.