Let’s take a look and see what we can do that is a step toward some kind of Inglenook Solver. This is made more difficult by the fact that I’m not sure yet just what I want to accomplish. We’ll discuss that as well.
Yes, our requirements are unclear. Yes, I, too, have read articles and books saying that requirements should be clear. I have also lived in the world quite a while and I have found that even the most clear requirements are usually not very clear, and even the most stable requirements change. And in this case, my whole purpose is to explore the Inglenook Shunting problem and to learn about it. I have a vague idea that I would like to build a sort of automatic switching yard in Second Life, but right now I’m just trying to understand how shunting works.
I’ve found i valuable to get used to working with changing and vague requirements. It encourages me to work in small steps, to show my work often, and to write code that is able to be changed readily. And that leads me to write a small advertisement for classes and objects.
When we use classes and objects, we group together both the data that represents some idea—the point of the class and objects—and the functions, methods, to operate on that data. That provides at least these advantages:
Focused objects are easier to test, since what they must do is clearly defined by the methods they provide.
It is easy to move objects from a test environment to a production one, since their methods are clearly indicated as belonging to the class.
When changes to the application are needed, those changes are often, perhaps even usually, limited to changes in just a few of the objects, which makes it easier to test and make the changes.
Thank you for reading this short advertisement for classes and objects.
Yesterday I plugged in the more robust class()
function for defining classes, and changed our simple Car and Siding classes to use it. Car has no real capability yet: it just carries an “id”, which I intend to be the value we’ll use to give them order, “1”, “2” and so on. Or whatever we may choose.
local Car = class()
function Car:init(id)
self.id = id
end
Siding has just one method so far, allowing us to push a car onto the siding, if it will fit:
local Siding = class()
function Siding:init(length)
self.length = length
self.cars = {}
end
function Siding:push(car)
if #self.cars == self.length then
return false
else
table.insert(self.cars, 1, car)
return true
end
end
What’s next? Should we review the tests here? Let’s not, but we will at least write some new ones. The existing six asserts are all running OK.
I think we should have a Locomotive class, representing the locomotive that shunts things around. My tentative thought is that it will include the intelligence to decide what to do, as well as to do it, although we may find it desirable to break out the planning function from the doing later on.
I prefer not to speculate too much about lots of objects that we “might” need. Instead, I let the code we write and the things we try to do show us the need for different data collections and different behavior. You are of course free to code however you see fit: this is my blog and my way.
So Locomotive. Let’s write a test. What can we say about the locomotive? I think it will have a collection of cars, and the Inglenook game will limit that to three, because while the loco might pull more than three, it can’t change any switches if it has more than three. Should we make the “3” a parameter to the class creation, or just a built in constant? Our Siding instances have limit
and it is a variable. Let’s stay simple for now and in Locomotive treat it as a constant. Here’s a trivial test to get us going:
function Tests:test_create_loco()
loco = Locomotive()
assert_equals(loco.limit, 3)
assert_equals(#loco.cars, 0)
end
Notice how easy it is to set up a new test: I just write a function on the Tests table and it will be run. Of course right now it won’t run because Locomotive isn’t defined. I’ll do that before I even bother to try the run.
local Locomotive = class()
function Locomotive:init()
self.limit = 3
self.cars = {}
end
Tests all run:
[05:22] Inglenook 0.4: Tests: correct 8, wrong 0
OK, now what. I’m imagining that the loco will drive up to one of the sidings and pull out one, two, or three cars. And since the loco is the brains of the operation, I think it actually will want to know all the sidings, so that it can talk to them.
For now, let’s give it an add_siding
method, with another near-trivial 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
We’ll just add a 5-siding and then check that we have it. And we implement:
local Locomotive = class()
function Locomotive:init()
self.limit = 3
self.cars = {}
self.sidings = {}
end
function Locomotive:add(siding)
table.insert(self.sidings, siding)
end
Our tests all run:
[05:33] Inglenook 0.4: Tests: correct 10, wrong 0
.
I strongly suspect that the loco won’t want just a simple list of sidings. I think it will want to access them intelligently and that a list won’t do the job. But I’m not sure what will do the job, and the list is the simplest thing I can think of, so we’ll go with it until we learn more. It’s all inside Locomotive, so if we have to change it, all the changes will be Loco-l, er I mean local.
Let’s do a test like this: Create a siding with 5 cars on it. Create a Loco, give the siding to the Loco. Have the loco “take” two cars from the Siding and then test that everyone has what they should have.
This is pretty ambitious by my standards but I think we’re up to it. But I change my mind. Writing the test, I see a simpler one to do first, one that just tests the Siding:
function Tests:test_siding_pull_two()
siding = Siding(5)
for i, name in {"1", "2", "3", "4", "5"} do
siding:push(Car(name))
end
two_cars = siding:pull(2)
assert_equals(two_cars[1].id, "5")
assert_equals(two_cars[2].id, "4")
end
I propose that the new method, pull
returns a array table of cars pulled. My test could also check that the cars are gone from the siding, but this is enough to let me code.
function Siding:pull(count)
if count > #self.cars then
return {}
end
result = {}
for i = 1, count do
table.insert(result, self.cars[1])
table.remove(self.cars, 1)
end
return result
end
This code returns an empty table if the count is too large. I know we return a true/false when you try to push too many things into a Siding, but here I think it’ll suffice to return nothing. An alternative would be to return as many as we have, and if that should turn out to be useful, we can change it.
Anyway the test runs. And, for now, there’s no reason to test again with Locomotive, we’re confident that the Siding will return the right items when anyone asks, Locomotive or not.
I am now faced with a kind of a dilemma. I feel like writing a test that gives a loco a couple of sidings and have the loco move a couple of cars from one to the other. But why would I do that? Push works. Pull works. Therefore there’s nothing to test … yet. OK, solved. no need to test that.
So what might be next? Way back when we started this Inglenook thing, we were talking about operations like Pull and Stash and Connect that could be used to do the sorting we’re trying to do. The idea was a “language” for expressing the plan of operations that gives us the train we want.
So this is interesting and kind of fun but is it leading quite where we want to go? Where do we want to go?
Here’s a tentative plan:
Work toward an in world visible display of switching. We’ll start by moving cubes around but perhaps go all the way to a switch yard using our real SL Valkyrie Transport trains.
Use these SLua objects, Locomotive, Car, Siding to represent and interface with Second Life objects, representing locomotives and cars and sidings that we’ll use to build an automatic switch yard demo. Again, probably starting with cubes, since we can’t run this code on the main grid yet.
Code to produce a plan of operation using these objects to produce and simulate the plan.
Possibly connect these objects to the SL objects in the switch yard and use the code directly, or maybe just port over the ideas.
Since SLua is an unknown time away, probably months away, we can’t really run this code on the main grid yet. So we’ll have to see whether to port it back to LSL (painful but the ideas can be re-implemented somehow I’m sure) or what.
So next steps, I think, will be to give the Loco enough information to let it start producing a plan, and then we’ll implement code that executes the plan, using methods like the push
and pull
we have now.
Still just feeling our way, learning SLua and learning what our program wants to be when it grows up.
I am very pleased with these tiny objects so far. I’m sure I’m going to enjoy working in SLua even more than I enjoy working in LSL. Maybe almost as much as I enjoy working in Python.
In each of these articles, you’ll have noticed that I only get a very few lines of code written per article. Today, about 25 not counting tests. That seems like slow progress, and if you look at the calendar, it is slow progress. But my purpose here is to share my thinking with you, and that takes a lot of writing and a lot of reading. If we were sitting together chatting as we code, we’d get more code written in less time. But here, I want to share my process and style with you, not just bang out code. When I’m just banging out code, I don’t write an blog article at the same time.
See you soon! Safe paths!