This morning I’d like to make a bit of progress toward solving the puzzle. I have at least two ideas for how we might proceed. Overall, I make a tiny bit of progress but not as much as I’d like. I do make the testing setup a bit better.
One possibility is to begin to build a demonstration object that actually moves some prims around. I envision an object with cube-like pieces representing cars and loco, and when the Loco decides what to do, the pieces move showing what happens. This won’t be terribly complicated, since I have moved a lot of cubes in my life here, but there’s not much learning in it for me, although I hope at least some readers will be interested in seeing how we do it.
Another possibility, and one that I think I prefer, is to work on the decision-making process. Our script, probably the Locomotive object, needs to understand the situation with all the sidings and cars and then to take actions to move cars around. My very tentative notion is that the decider will discover that it wants to do something, say swap the first two cars in the 5-siding. Starting from the resting position, what needs to happen is something like this:
I think, but am not certain, that this is the easiest move we’ll ever see. And I have finessed the issues around moving the cars already in a siding when we push new cars into them.
At this writing, I am not at all certain that we can teach the script to figure out those steps. What I’m sure we can do is to figure out sequences like this with various objectives: swap first two, swap second two, and so on, and that we can figure out by looking for inversions which sequence to apply and then apply it. The individual steps almost seem to me to be able to be scripted in a straightforward fashion, with a bit of hand-waving about pushing cars back into the sidings when we push one in … we would not like the display to show the cars overlapping and only later moving.
Let’s try to write a test. This will be a “story” test, as some people call it, not a simple test that checks that fundamental operations work. I’ve been taught that these tests are undesirable. In particular, they tend to need to be updated often, and in complicated ways. So we’ll try to avoid most of these. Our purpose here will be just to begin to figure out what the methods might look like for doing this job.
My first sketch looks like this:
function Tests:test_pull_one_car()
siding5 = Siding(5)
siding31 = Siding(3)
siding32 = Siding(3)
siding.set_cars("2", "1", "3" "4"."5")
loco = Locomotive(siding5, siding31, siding32)
plan = loco.fetch(siding5, 1)
-- assert things about plan
end
I’ve very tentatively made some design decisions here. I’ve supposed that there is a method set_cars
on Siding, allowing us to specify the car names in that siding. I’m envisioning that reading from let to right, not reversed as when I pushed them in one at a time in a prior test. That notion is probably pretty solid.
And I’m supposing that when we create a Locomotive, we give it the sidings. For now, we’re assuming that the brains are in the locomotive, so it needs to know that. I am less certain about this idea.
Then I’m supposing that the locomotive can be told to fetch
a car from a siding. I intend this to mean “starting from your resting position, go to the named siding, pull one (additional) car, and return to your resting position.” When the locomotive is told to do this, it is supposed to return a “plan”, which we can somehow check for correctness.
What is going on here? We are inventing a language, a set of verbs that we can use to tell the various objects in our script what to do, and, ultimately, how to do it. Since we can associate methods with each object, we can think in terms of giving each object a vocabulary of methods, things it knows how to do.
Why is it so tentative? Because making up language is tricky and the more we work on it the better we can make it. So we try methods and see how things go, and if we see how things could go better, we change things. Working that way, with a willingness to change and even replace our methods, we can iterate and converge on a better design than we could possibly come up with at the beginning.
What is that plan
? I’m not sure but let’s pretend we can look at it and make assertions about it, like this:
function Tests:test_pull_one_car()
siding5 = Siding(5)
siding31 = Siding(3)
siding32 = Siding(3)
siding5:set_cars("2", "1", "3", "4", "5")
assert_equals(siding5.cars[1].id == "2")
loco = Locomotive(siding5, siding31, siding32)
plan = loco:fetch(siding5, 1)
assert_equals(plan[1], "moveto 5")
assert_equals(plan[2], "pull 1")
assert_equals(plan[3], "moveto L")
loco.execute(plan)
assert_equals(loco.position, loco.positions["L"])
assert_equals(#loco.cars, 1)
assert_equals(loco.cars[1].id, "2")
end
I’ve sketched two kinds of checks here. First, I look at the plan to see if it has commands in it that do what I think it should do. I just gave the commands strings, although I have in mind something far more exotic when we get to it.
But when we think of how we’ll implement fetch
, won’t we just do a couple of string substitutions and emit that list? This check is hardly checking anything.
The second batch of checks at least checks the end result: is the locomotive in position “L” (my made-up name for its rest position), does it now have just one car, and is that car #2, the first car that we had in the 5-siding?
I do not like this test. I think part of it might be OK, so let’s try to make some of it work.
I commented out the last bits, so this test runs now with code I’ll show just below:
function Tests:test_pull_one_car()
siding5 = Siding(5)
siding31 = Siding(3)
siding32 = Siding(3)
siding5:set_cars("2", "1", "3", "4", "5")
loco = Locomotive(siding5, siding31, siding32)
plan = loco:fetch(siding5, 1)
assert_equals(plan[1], "moveto 5")
assert_equals(plan[2], "pull 1")
assert_equals(plan[3], "moveto L")
-- loco.execute(plan)
-- assert_equals(loco.position, loco.positions["L"])
-- assert_equals(#loco.cars, 1)
-- assert_equals(loco.cars[1].id, "2")
end
Setting up the Siding:set_cars
was easy enough:
function Siding:set_cars(...)
for i, id in {...} do
table.insert(self.cars, Car(id))
end
end
As you’ll see, I just faked the fetch:
function Locomotive:fetch(siding, item)
return { "moveto 5", "pull 1", "moveto L"}
end
This may look like cheating, but often when the next step we want to take is a big one, we can make an intermediate step by just returning a constant, as we did here. Then the next test we write for fetch
will require us to start putting in more intelligence. Kent Beck called this idea “fake it till you make it”, and it’s often very useful for getting methods in place without needing to make them work completely.
Adding the parameters to the Locomotive creation was also straightforward, but it’s also wrong:
local Locomotive = class()
function Locomotive:init(s5, s31, s32)
self.s5 = s5 or Siding()
self.s31 = s31 or Siding()
self.s32 = s32 or Siding()
self.limit = 3
self.cars = {}
self.sidings = {}
end
To make my test pass I needed to pass in specific sidings, but there is already a sidings collection in the object and a method add(siding)
and at least one test. Let’s sort that out. I do think that having specific members for the three sidings will make things easier, so let’s change the add
to use those members. (This could be a mistake, but if it is, I don’t think it’ll be a big one.)
Here’s what I think I want:
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
This will surely break a test, but I’ll save it to see what all happens. Lua says:
lua_script:182: attempt to get length of a nil value
lua_script:182 function test_add_siding
lua_script:147 function run_tests
lua_script:110 function state_entry
OK, we’ll actually look at the test:
function Tests:test_add_siding()
loco = Locomotive()
siding = Siding(5)
loco:add(siding)
assert_equals(#loco.sidings, 1)
assert_equals(loco.sidings[1].length, 5)
end
Let’s just make the test right and make it work.
function Tests:test_add_siding()
loco = Locomotive()
siding5 = Siding(5)
siding31 = Siding(3)
siding32 = Siding(3)
loco:add(siding5)
loco:add(siding31)
loco:add(siding32)
assert_equals(loco.s5, siding5, "siding 5")
assert_equals(loco.s31, siding31, "siding 31")
assert_equals(loco.s32, siding32, "siding 32")
end
You’ll perhaps notice the new parameter on assert_equals
. That’s because the assertion just prints the values and results and couldn’t tell which one was wrong. So I added an optional parameter to assert_equals
:
function assert_equals(result, expected, message)
local message = message or ""
local correct = result == expected
if correct then
test_correct += 1
else
test_wrong += 1
print(`{message}:{result} == {expected} {correct}`)
end
end
I’ll do the same with the other assertions. When the tool doesn’t quite serve, improve the tool, right?
The message tells me that s31 is wrong. The defect is due to the fact that I defaulted the sidings in setting up the locomotive. The correct init is 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
My tests are all running. I’ve not made much progress, but I think I have made a little. The Locomotive now knows its three sidings as individual named items, which I think will serve us better. And there’s a sketch of what producing a plan might look like.
What I don’t like and need to think about is what approach would be better than setting up a bunch of scenarios and checking the plans manually. It may come down to that, but smaller tests than that are easier to write and tend to be more stable.
There’s another issue. In a reasonable language with a reasonable IDE, I’d have all these tests and classes in separate tabs in my editor, so that when I want to edit Locomotive I just have to click that tab and there I am. In SLua everything is in one big file, so I wind up scrolling around a lot. I’d like to find a better way. Perhaps just opening the file in several tabs in Sublime Text will be better. I’d like to figure something out.
Overall, I feel unsatisfied with progress, but I’m inventing something I’ve never done before, so slow progress is better than total confusion. And I am a bit proud of myself for improving the testing setup a bit, with the optional messages. Offline I’ll adjust the other asserts and save the setup off in its own file for reuse.
Safe paths!