I think I’m ready to actually code up some shunting. Lots of design thinking here, and a tiny bit of code.
Tool-wise, I have two small frameworks, one for creating classes and one for testing, plus three classes, Locomotive, Siding, and Car, and a suite of tests called Tests. In a real IDE, I would have each of those things in a separate tab, and would not even have the class creation or testing framework open at all unless I was modifying them. In Sublime Text, all of that is in one file, and it’s 275 lines long, far too long to be scrolling around all the time. I’d like to have some automated way of merging things back together before compiling. A Sublime plugin could probably do it, but now is not the time.
Here, just in case you want to read the end of the mystery first, are today’s new test and the code that makes it pass.
Test:
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()
assert_equals(plan, "m1 p1 home m2 p0 home m1 p1 home m2 p2 home m1 p0 home")
end
Code:
function Locomotive:plan_one_step()
return self:plan_inversion_at_1()
end
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
function Locomotive:assemble(...)
return table.concat({...}, ' ')
end
It took some thinking and explaining to get there. Here are the details:
Now is the time for more shunting. I think I’m ready to do actual shunting (in the model, not in visible moves of prims, not yet). Here’s my plan, roughly:
With #1 and #2 in place, we can solve any sequence of cars with no blockers (and an assumed structure of blocks in the 3-sidings). With #3, we can solve any Inglenook problem.
Here’s what I’ve figured out. Suppose we have this structure in the sidings:
21345
xx
x
The way we’ll solve this is to pull the 2 over to the second 3-siding and leave it, then go back and pick up the 1, then back to pick up the 2 and take them both back to the 5-siding.
With this problem:
13245
xx
x
The solution is almost the same: Pull (the 1 and) the 3 over to the second 3-siding and drop it off, then go back and pick up the 2 (train is now 12), then back to pick up the 3 (now 123) and take them back to the 5-siding.
So when the inversion is in one of the first two positions, we pull out the high value and cache it in a 3-siding, then go back for the low value, then back to the 3-siding, pick up the high, take everything back to 5-siding.
What about inversions in the third or fourth position? That’s where we do the famous mathematical trick, reducing the problem to one previously solved. We pull two cars out of the 5-siding and park them in the third 3-siding, which has room for them. Now the inversion is in either the first position or the second position in the 5-siding. We solve it, then go back to the 3-siding for the first two cars and put them in the 5-siding. It goes like this:
12354
xx
x
Move two:
354
xx
12x
Now we know how to solve this, it’s the same as 132. Move the 5 to the second 3-siding …
34
5xx
12x
Grab the 4, pick up the 5, move both to 5-siding …
345
xx
12x
Go back to the third 3-siding for the 12
12345
xx
x
Solved!
OK, that’s the good news. But the devil, they say, is in the details. Let’s see what our overall algorithm is. It must be something like this:
Now, as written, there are details left out, like stopping as soon as there are no inversions, and it seems that it would be wise not to do step 5 until there are no inversions in the last three, although it will be amusing to see it thrashing.
Idea: If we had the plan written out in some form, we might see something like:
move 2 cars from second 3-siding to 5-siding
move 2 cars from 5-siding to second 3-siding
A clever program might see those operations and cancel them out. There might be other such cases. We’ll try to keep that in mind … but I think the best plan is to make it work, make the code decent, and then see about making it more efficient. Writing clever code all in one go is risky and leads to more debugging than I like.
OK, that’s more planning than I’d usually do, but I wanted you to understand where we might be going. I say “might” because if we learn something along the way, we’ll adapt our plans.
Let’s take a look at our tests now and see if we need to normalize things to make progress easier. Often it’s best to smooth the ground before we start building new things, and I think today is one of those days. Remember, for example, we have that odd thing with the look up of the siding by index:
function Locomotive:take(siding_number, count)
local siding = self:get_siding(siding_number)
local taken = siding:pull(count)
self:append(taken)
end
function Locomotive:get_siding(siding_number)
local ss = {self.s5, self.s31, self.s32}
return ss[siding_number]
end
Let’s put the sidings directly into an array. That may break a test. I could look but when it breaks, I’ll look then.
Change this:
local Locomotive = class()
function Locomotive:init(s5, s31, s32)
self.s5 = s5
self.s31 = s31
self.s32 = s32
self.limit = 3
self.cars = {}
end
To this:
local Locomotive = class()
function Locomotive:init(s5, s31, s32)
self.sidings = {s5, s31, s32}
self.limit = 3
self.cars = {}
end
We also have this:
function Locomotive:add(siding)
if siding.length == 5 then
self.s5 = siding
elseif self.s31 == nil then
self.s31 = siding
else
self.s32 = siding
end
end
Let’s just see who’s using this and make them stop. This test gets revised. Remove the add, see if we stay good.
function Tests:test_add_siding()
siding5 = Siding(5)
siding31 = Siding(3)
siding32 = Siding(3)
loco = Locomotive(siding5, siding31, siding32)
assert_equals(loco.s5, siding5, "siding 5")
assert_equals(loco.s31, siding31, "siding 31")
assert_equals(loco.s32, siding32, "siding 32")
end
I’m not terribly surprised to have something break. I am used to modifying code and letting the tests tell me where changes are needed.
Inglenook 20250514 [script:inglenook_20250514] Script run-time error
runtime error
lua_script:107: attempt to index nil with 'pull'
lua_script:107 function take
lua_script:255 function test_take
lua_script:179 function run_tests
lua_script:140 function state_entry
lua_script:261
LOL, I forgot to fix this:
function Locomotive:get_siding(siding_number)
local ss = {self.s5, self.s31, self.s32}
return ss[siding_number]
end
function Locomotive:get_siding(siding_number)
return self.sidings[siding_number]
end
Three assertions fail, these three:
function Tests:test_add_siding()
siding5 = Siding(5)
siding31 = Siding(3)
siding32 = Siding(3)
loco = Locomotive(siding5, siding31, siding32)
assert_equals(loco.s5, siding5, "siding 5")
assert_equals(loco.s31, siding31, "siding 31")
assert_equals(loco.s32, siding32, "siding 32")
end
That’s really useless now, we don’t even have s5
and all that. Remove that test. All the rest run.
Now we really have to figure out a next step, the next test to write. Let’s think a bit about our “algorithm”. We’ll assume we’re at the state where there is an inversion in the first three cars. There are two cases, either the first pair has an inversion or it does not and the second pair does. (Both are possible: 432 would do it, but the first has priority, because why not?)
(There is probably a clever way to convert 432 to 234 without all the work we’ll do with the current scheme but my mission is to get to some solution and then make it better.)
The actual operation is different between 243 and 432. To fix the 432 case we move the 4 off, drop it, then drag the three to it, but in the 243 case we drag the 2 and 4 off, drop the 4 off, then drag 23 over to the 4, pick up the 4, and put all three back.
I would like to find a way to express those two cases the same way, so that it just takes a deeper bite and carries on.
That’s too much. I could probably figure it out but let’s do the simple case and then improve it. Our current scheme seems to be to produce a textual plan that we can test, which we will then execute. Let’s do that with this new case, and invent a bit more of our plan language as needed.
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()
assert_equals(plan, "m1 p1 m2 p0 m1 p1 m2 p2 m1 p0 mh")
end
Let me expand on that a bit. I’m assuming that when we move to a siding, we push in far enough that we are connected to the whole train that’s in there. So we specify the total number of cars we want to pull out. So “p0” means to leave everything there and come away with no cars. I think we can avoid having a drop command at all, we’ll just specify the number of cars we want in the train at that point.
Which will lead to an interesting end condition: when we think we’re done the plan will include p5 and we can check at that point to see if the train is valid.
I think we should specify the moves to home. We have to go home to move from siding 1 to siding 2 and so on. Expand the test:
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()
assert_equals(plan, "m1 p1 mh m2 p0 mh m1 p1 mh m2 p2 mh m1 p0 mh")
end
I don’t like the mh not standing out. Let’s have it compile “home” instead of “mh”.
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()
assert_equals(plan, "m1 p1 home m2 p0 home m1 p1 home m2 p2 home m1 p0 home")
end
Now I’m going to use “fake it till you make it here”, at least at first, but I’ll try to put a little structure into the implementation even so.
function Locomotive:plan_one_step()
return self:plan_inversion_at_1()
end
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
function Locomotive:assemble(...)
return table.concat({...}, ' ')
end
I’m figuring there will be other plan
methods, but for now there’s just one. Before we’re done, we’ll be checking and selecting with plan to run. The plan inversion method breaks down the operation into separate steps with possibly reasonable names and the steps to do that thing. And then I had to write assemble
because otherwise I would have to space or not space.
However. I don’t think this will last long. It’s certainly possible to do our execute
command by parsing this string. But I think it’ll be better to build a set of fundamental operations that do things, build an array of those, and have them have a display format that produces our test string. Why? Because the string is for my personal convenience and an array of operations to do will be better for the actual program. So we’ll keep those ideas separate.
This plan_inversion_at_1
is still somewhat fake but I think we can see how it is becoming less so, as we have identified some specific operation steps that the locomotive could actually perform.
There is another fake test, left over from a day or so ago, this one:
function Tests:test_pull_one_car()
siding5 = Siding(5, "21345")
siding31 = Siding(3)
siding32 = Siding(3)
loco = Locomotive(siding5, siding31, siding32)
plan = loco:fetch(2, 1)
assert_equals(plan, "m2 p1 mh", "check plan")
end
I think the fetch
notion has served its purpose, getting me off the dime to produce an operation string. But we don’t need it any more: our new test is better, leads further toward real code. Remove this test and the fetch
method.
I think we’re done for the morning. Lots of words to explain my thinking, plus a small bit of progress toward a well-structured program. This may seem slow to you, but most of the slowness is that I’m explaining my every thought. The actual coding this morning was perhaps 15 minutes, counting looking for all the places where I wrote .
and should have written :
.
Next time, I think we’ll do a bit of generalizing to handle the second nearby-inversion case. And maybe we’ll move toward a more executable form, with the strings just being used for display.
Until then, safe paths to you!