This morning I plan to build the plan that swaps cars 2 and 3, since doing 1 and 2 worked yesterday. Then we probably need some design and refactoring.
We have this test running:
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
Let’s see if I can do the 2-3 inversion offhand. New test:
function Tests:test_reverse_two_three_via_plan()
siding5 = Siding(5, "13245")
siding31 = Siding(3)
siding32 = Siding(3)
loco = Locomotive(siding5, siding31, siding32)
loco:plan_inversion_at_2()
loco:execute_plan()
self:assert_equals(loco:siding_car_string(1), "12345", "sorted!")
self:assert_equals(loco:car_string(), "", "loco", "empty")
end
I renamed the other one to test_reverse_one_two_via_plan
. Couldn’t think of a longer name. Now I need a new method. I’ll do the empty solution to get the test to fail.
function Locomotive:plan_inversion_at_2()
self.plan = {}
end
I have to create an empty plan or the test will not run to completion. I should get a standard failure now.
[05:28] Inglenook 20250516: test_reverse_two_three_via_plan: 13245 was expected to be 12345 (sorted!)
Perfect! Now let’s do the work. The sequence is rather like swapping 1 and 2, except that we pull an extra car all the time.
function Locomotive:plan_inversion_at_2()
self.plan = {}
self:add_action(self.move, 1)
self:add_action(self.pull, 2) -- #1, #2 | #3
self:add_action(self.move, 2)
self:add_action(self.pull, 1) -- #1
self:add_action(self.move, 1)
self:add_action(self.pull, 2) -- #1, #3
self:add_action(self.move, 2)
self:add_action(self.pull, 3) -- #1 #3 #2
self:add_action(self.move, 1)
self:add_action(self.pull, 0)
end
As soon as I replaced my dots with colons, this worked first time.
Let’s take a more broad look at the situation. Here are some issues that I see right offhand:
The plan needs to be initialized outside the individual plan...
operations, so that we can create a plan using more than one (and I am sure that we’ll want to do that.)
The sequence move
, pull
occurs a lot. If nothing else a new method add_move_and_pull
or something might be in order. Preferably something with less typing but still clear enough.
We repeat self:add_action(self.
quite a lot, which suggests to me that we need a better notation. Maybe the right word for the idea above will serve.
Our first method returns a string representing the command sequence, used in a separate test. We’re going to want to test each command sequence to see that it works as intended. Perhaps the string idea no longer sparks joy and should be thanked for its service and kissed up to heaven.
The program will move to a setup where we are presented with a random arrangement of cars and the planning function decides which “primitive” operations to perform. Our current two plans are examples of primitives. If we had primitives for inversion at 3 and inversion at 4, just two more primitives, we can solve any situation with the five numbered cars in the 5-siding. Some solutions will be slow but not compared to the solution we had before which was that we couldn’t solve it at all, so don’t be calling my solution inefficient.
Still, it would be good to have a better way to proceed than bubble sort. The trick is to recognize patterns and apply then in an order that reduces our total number of moves. We’ll see what we can figure out. Right now I feel good because I’m confident we can write a program that will at least solve the problem.
Let’s write a new method go_get
to simplify our code, like this:
function Locomotive:go_get(siding_number, car_count)
self:add_action(self.move, siding_number)
self:add_action(self.pull, car_count)
end
Now we can use this in our existing plan methods:
function Locomotive:plan_inversion_at_2()
self.plan = {}
-- self:add_action(self.move, 1)
-- self:add_action(self.pull, 2) -- #1, #2 | #3
self:go_get(1, 2)
-- self:add_action(self.move, 2)
-- self:add_action(self.pull, 1) -- #1
self:go_get(2, 1)
-- self:add_action(self.move, 1)
-- self:add_action(self.pull, 2) -- #1, #3
self:go_get(1, 2)
-- self:add_action(self.move, 2)
-- self:add_action(self.pull, 3) -- #1 #3 #2
self:go_get(2, 3)
-- self:add_action(self.move, 1)
-- self:add_action(self.pull, 0)
self:go_get(1, 0)
end
Note that I commented out the old code and then inserted my new calls one after each pair, giving me a reasonable chance of getting it right. Save and see if this runs. And after I replace dots with colons, it does run. Fix up the other plan and remove the comments.
Along the way I decide to remove the test for the string-style plan. My new plan methods are nearly good:
function Locomotive:plan_inversion_at_1()
self.plan = {}
self:go_get(1, 1)
self:go_get(2, 0)
self:go_get(1, 1)
self:go_get(2, 2)
self:go_get(1, 0)
end
function Locomotive:plan_inversion_at_2()
self.plan = {}
self:go_get(1, 2)
self:go_get(2, 1)
self:go_get(1, 2)
self:go_get(2, 3)
self:go_get(1, 0)
end
But I noticed something odd. My test reports in chat look like this:
[05:58] Inglenook 20250516: Tests: correct 25, wrong 0
[06:01] Inglenook 20250516: Tests: correct 25, wrong 0
[06:05] Inglenook 20250516: test_first_inversion: nil was expected to be m1 p1 home m2 p0 home m1 p1 home m2 p2 home m1 p0 home ()
[06:05] Inglenook 20250516: Tests: correct 24, wrong 1
[06:05] Inglenook 20250516: Tests: correct 26, wrong 0
Before I removed the string checking test, we had 25 correct, zero wrong. I removed the code to produce the string and that changed to 24 correct and one wrong, as one might expect. But when I removed the test, the report changed to 26 correct! I’d have expected 24 correct, zero wrong.
What’s up with this? We need to understand it, there is something going on that I can’t explain. This is bad. First, I put the test back. The report returns to 24, 1. Now let’s see if we have a verbose mode.
We have. It appears that test_reverse_two_three_via_plan
is running twice with the test removed.
After some work trying to generate a bug report, I manage to isolate the problem to my code, which is good news.
This method:
function Tests:run_tests(verbose)
test_correct = 0
test_wrong = 0
for k, v in Tests do
if k:sub(1, 5) == 'test_' then
Tests.current_name = k
if verbose then print(k) end
v(Tests)
end
end
print(`Tests: correct {test_correct}, wrong {test_wrong}`)
end
The line that sets current_name
is adding a key to the table Tests, inside a loop over the table! This is an explicitly improper thing to do. You can change a key’s value, and I think you can even remove a key, inside the loop, but you cannot add keys. The fix is to initialize it in the original creation of Tests:
Tests = {current_name=""}
And we only see each test run once. Well-spotted, Janet!
Let’s close this out on a couple of high notes.
I found a bug in my testing framework. In play it caused a test to run twice, but I suspect it could just as easily cause a test to be skipped. Or explode a kitten: the behavior is undefined. I found it by removing all the content of all the tests and just looking to see if the test name printed twice. I was preparing a bug report for LL, so I wanted to trim things down as far as I could.
I removed all the classes and the class
function, still the duplicate test. Then I removed one of the methods of Tests, I think diagnose
, and suddenly the test didn’t appear. I put it back. Enough fiddling like that and I finally noticed the setting of the string name into the Tests table. Initialized that and all was good.
I am very confident that I have found the issue. I am not bet your life confident, because I never am, but I think that was the issue. Certainly it was the only thing “wrong” in my code and fixing it fixed the problem. Since it’s a Heisenbug by its nature, we can’t be sure we got it.
More the point of the day, another inversion can be uninverted, and the code is a bit more compact with the new ge_get
method. I suspect it could be even more compact, and as we code up more primitives we’ll keep a look out for opportunities to simplify our work while keeping the code understandable.
A good morning. Time for some caffeine and a bit of reading. Until next time, I wish you safe paths!