Robot 2
What do skeletons have to do with robots? Could there be more than one skeleton?
In one of his videos about the Robot Worlds, Hill begins to create a “walking skeleton”. This is a term from Alistair Cockburn1, referring to a very spare version of the desired product, one that may do very little, but that is in some reasonable sense, a working model of the program, from top to bottom.
Hill chooses, as his walking skeleton, a little program that sends some message across the socket and gets a reply back. This makes some sense to me: I might think: right, once I can get messages across the wire, I can surely write programs that process those messages.
I have chosen another place to start. My walking skeleton is, or will be, a World and a Robot interacting. I imagine that that makes sense to Hill, who thinks something like: right, once we get objects that do the work, we can surely connect them across a network.
If he is wise—and he is—Hill also thinks “Jeffries had better be careful to keep his objects separated enough to allow for messages to be wedged in between those calls”. If he is wise—and he is—he also thinks “Even if he doesn’t remember to do that, it’ll be interesting to see how he refactors his way out of trouble”.
Now I don’t use the term “walking skeleton” often, but frequent readers are certainly aware that I try to get a running version of the program as soon as I can, and to keep it running thereafter, as I improve it. Hill and I have that habit in common: This Is The Way. But how do we pick a walking skeleton, and how can it be that two almost identically handsome and intelligent gentlemen like Hill and myself choose entirely different, indeed almost opposite walking skeletons for the same program?
Kent Beck once said that all programming methodologies are based on fear, the fear of the method’s designer. They design a method to counter their own biggest fears.
To the extent that that’s true, I’d say that we must all have in common one primary fear: the deadline may come, and we may have nothing to ship. We resolve that fear by ensuring that we get something to ship as soon as possible, and we make it better by adding capabilities, most valuable first. Doing this, we ensure that we have the best possible product ready to go on the date. This, too, is The Way.
But, as experienced programmers, we have failed in myriad ways, and so we have almost innumerable fears. Lurking in the fog that is our initial understanding of the problem and solution, there are many monsters, problems that will one day appear and need to be solved. Because we are experienced, we have a good idea what those problems are, and we’re generally pretty sure we will find a way to solve them.
One good thing to do, and Hill chose it, is to pick one of those scary problems first, and solve it. Once we know how to defeat that monster, we can set it aside and move on to other matters. If we pick the most terrifying monster first, and defeat it, there’s a good chance that the worst is over. There will be many more monsters—but not that one.
Another good thing to do, and I chose it, is to pick easy problems first, get them under control, and repeat until the scary monster’s shape becomes clear in the fog. Then we attack and defeat the scary one.
Both ways carry risks. If we choose sending a message between server and client, as did Hill, we risk that our stakeholders, who might be looking for a game, will not see the value of the message sending. And, we risk that we’ll build the message sending up into a shape that doesn’t really fit our application, so that the rest of the application has to go around all crouched over so as to fit the message sending solution.
If we choose the game, as I did, we risk that our stakeholders will think we must be nearly done, and that the hard problem will be deferred forever. And we risk that we may not be able to solve the communication problem at all! That would be fatal. Finally, we risk that we’ll build our game in some way that makes it very difficult to connect its components over a network.
How do we choose? We can imagine ways: decide based on the experience of the wisest person in the room; decide based on the opinion of the highest paid person in the room; decide at random; pick something easy; pick something hard. But in the end, to me, these all come down to working together to talk about the issues, not for months but for hours, and to jointly pick something.
Over at Hill’s place, I guess the decision came down to him, and to Wally and Molly. Here at my place, it came down to me and to Kitty, and Kitty just wanted to go out on the deck.
In the end, we use our best judgment to pick a walking skeleton, and get to it. We keep as many scary issues in mind as we can without freezing from fear, and we get on with it. And one very important thing: whatever we build, we build it well, with all nicely separated concerns, and the clearest code we can write, so that as the scary monster problems do appear, we can incorporate the solutions readily.
If we do this well, we’re great. If we do it poorly, we have things to learn. Me, well, I’m hemi-semi-demi-great, and I sure as heck do have things to learn, and relearn, and relearn, every day.
Let’s get to it. I have my banana, granola bar, and chai ready, so let’s see what we should code. I have learning to report, and then we’ll think of something to do.
Info from the Spec
I have learned some important things about the original spec. When a robot does a scan, it only sees things along the x and y axis of where it is currently sitting. That is, it sees up and down, left and right, but not diagonally.
Furthermore, the scan results will always come back reported as North South East West, with North being whatever direction the robot is facing. Not true north, no, that would be too easy. But the robot radar screen is always “forward up”, not “north up”, so there we are.
We’ll need to accommodate the rotation bit in the Robot, when it scans, so that it puts things where they belong in its Knowledge, and we’ll need to be sure when we return information from the World, that we only provide what the spec says.
Oh, and if I’ve read the spec correctly, the robot does know its current true location in the world. That information comes back in some or all of the return information packets from World.
We’ll need to work with this information in mind, of course. So far, I don’t see much to concern ourselves with, until we get around to implementing the World part of scanning.
One more thing that I learned is that the spec provides pretty solid information about the message packets that go back and forth. We’ll see what those are and figure out how to accommodate them as we go along. We know they are JSON-compatible, which tells us that there can be named values, including named arrays, and key-value pairs. Nothing we can’t handle, and we’ll defer the JSON as long as we can, perhaps forever or longer. We just have to be sure there are places to splice it in.
Let’s see what we can code this morning: it’s already 0950.
World Contents
I want to start on giving the world contents. Recall that there are OBSTACLES, PITS, MINES, ROBOTS, and EDGES in the world.
Yesterday, we got the World class to allow creation providing width and height, and we ensured that both width and height were constrained to be odd. This allows for the world to be symmetric around 0,0. I might have gone another way.
Let’s think about that for a moment. If we follow the spec exactly, the x coordinates can vary from -(width-1)/2 to (width-1)/2, and the y can vary from -(height-1)/2 to (height-1)/2. Those expressions are not convenient, but of course once we calculate them for a given world, they are unchanging. Is there some advantage to this design of the world? I can imagine that someone had some graph paper, and drew axes on it and started sketching robots and obstacles, as they designed the game they wanted. They drew the axes in the middle of the paper, as one does.
Unless one is a computer programmer. Then, depending on the language one uses, one draws the axes in the top left of the paper (Python, for example) or the bottom left (Codea Lua, for example), because that’s how the screen display code works. In almost all languages, x increases to the right, and in some, y increases from top to bottom (boo!) and in others from bottom to top (yay!)
It’s tempting to care about this. I’d certainly prefer dealing with coordinates that match my computer and language, but I’m a big boy and I know that there’s no big difference between the choices. The biggest difference is that on some computers, North is increasing y (yay!) and on others, North is decreasing y (boo!). Even there, a quick multiply by -1 should do the job.
In the end, our screen coordinates don’t matter. We’re not going to display point 0,0 and 1,0 in the game as two adjacent pixels on the screen: the game would be impossible to play at retina resolution. We’ll be scaling and such anyway, so while we might be going “dirty fraggelrats, we’ll have to invert the coordinates”, it’s really no big deal.
N.B. Above we see an example of the thinking we do all the time. We see things, we think about them a bit, we set them aside. Maybe we make a note if we need to. Mostly we’re just filling our mind with the imaginary reality of whatever program we’re creating, building our intuition about it. That starts on the first day, and continues forever. We may wake up one morning in 2073 with an idea for this program. I certainly hope that that happens to me, assuming I’m in good health and not some brain in a jar or something.
But we were going to put some contents into the world.
World Contents, Really
The spec says that obstacles are defined by a rectangle, specifying two points, top left and bottom right. Everything in that rectangle will be an OBSTACLE. Let’s write a test for that.
I start with this, rather large but expresses the problem:
_:test("rectangular obstacle", function()
local world = World(25,25) -- -12 to 12, thanks guys
world:createObstacle(-2,10, 3,8)
for x = -2,3 do
for y = 8,10 do
_:expect(world:factAt(x,y)).is("O")
end
end
end)
Here I’ve invented two World methods, createObstacle
and factAt
. We recognize factAt
from the Robot side, and yes, I do plan to use the Knowledge class here. And we’ve decided that the facts will be simple letters, at least for now, O for obstacle. We might need something more, and we’ll deal with that when it happens.
Now this is a pretty big test, but I feel up to it, so let’s “just do it”. I’ll use running the test to drive out some work.
9: rectangular obstacle -- TestWorld:89: attempt to call a nil value (method 'createObstacle')
Now I begin to see that this really is a pretty big deal … I don’t even have a place to put obstacles as yet. I decide on a prudent course. I set this test to ignore
and write a simpler one:
_:test("place and detect one obstacle", function()
local world = World(25,25)
world:addFact(-2,10,"O")
_:expect(world:factAt(-2,10)):is("O")
end)
That looks easier. Test to see we don’t have addFact
:
10: place and detect one obstacle -- TestWorld:99: attempt to call a nil value (method 'addFact')
Add enough to make that work:
function World:init(width, height)
self._width = self:validDimension(width or 1)
self._height = self:validDimension(height or 1)
self._knowledge = Knowledge()
end
function World:addFact(x,y,content)
self._knowledge:addFact(x,y,content)
end
I expect to find that we don’t know factAt
: But I am mistaken, because I forgot that addFact expects a Fact, so my code needs to be:
function World:addFact(x,y,content)
self._knowledge:addFact(Fact(x,y,content))
end
Note that I decided that world wants to refer directly to x y content, and wraps them in facts inside its addFact method. That may be a mistake. And possibly, the Fact may be a mistake. We’ll see if it gets in the way. Anyway, test:
10: place and detect one obstacle -- TestWorld:105: attempt to call a nil value (method 'factAt')
We implement:
function World:factAt(x,y)
return self._knowledge:factAt(x,y)
end
Test, expecting success. A truly weird error appears:
10: place and detect one obstacle -- CodeaUnit:80: attempt to sub a 'table' with a 'string'
That’s an error out of CodeaUnit itself. I’ve never seen that before. I’m going to at least take a look inside CodeaUnit, since I can.
local is = function(expected, epsilon)
self.expected = expected
if epsilon then
notify(expected - epsilon <= conditional and conditional <= expected + epsilon)
else
notify(conditional == expected)
end
end
Line 80 is the upper notify
and it means that it thinks we’re doing a floating point compare with an epsilon provided. That makes me think I have a syntax error in my expect
. Back to RW and let’s see.
_:test("place and detect one obstacle", function()
local world = World(25,25)
world:addFact(-2,10,"O")
_:expect(world:factAt(-2,10)):is("O")
end)
Sure enough. Colon instead of dot. Should be:
_:test("place and detect one obstacle", function()
local world = World(25,25)
world:addFact(-2,10,"O")
_:expect(world:factAt(-2,10)).is("O")
end)
Test. Passes, as expected. We have an ignore but the local rules are that we can commit with ignored tests, though we may have to change that rule. I want to lock a save point. Commit: World has addFact and factAt.
As I created that commit message, I thought about range checking. We could, in principle, ask for a fact at 1000,1000. Does knowledge care? No, it just returns nil if it doesn’t have a fact. So let it be. Now let’s unignore our big test and see if we can make it work:
_:test("rectangular obstacle", function()
local world = World(25,25) -- -12 to 12, thanks guys
world:createObstacle(-2,10, 3,8)
for x = -2,3 do
for y = 8,10 do
_:expect(world:factAt(x,y)).is("O")
end
end
end)
Should be easy now. We might want to worry about values out of range or inverted or whatever, but not yet. This test doesn’t need those. We need more tests, probably.
9: rectangular obstacle -- TestWorld:98: attempt to call a nil value (method 'createObstacle')
Do it:
function World:createObstacle(left, top, right, bottom)
for x = left,right do
for y = bottom,top do
self:addObstacle(x,y)
end
end
end
This will fail with no addObstacle
:
9: rectangular obstacle -- TestWorld:23: attempt to call a nil value (method 'addObstacle')
And …
function World:addObstacle(x,y)
self:addFact(x,y,"O")
end
I expect success. I get success. Commit: World:addObstacle(left, top, bottom, right).
Since this call comes from outside the safe zone, the code we wrote and tested, we’d like to make it far more robust. A key question is what we should have it do if the parameters don’t make sense. The answer is, we’ll do nothing. The spec may call for an error to be returned, but I don’t know for sure and I don’t want to deal with it just now.
What are the things we might want to check?
- top > bottom
- left < right
- top and bottom are inside height limits
- left and right are inside width limits.
We could clip the obstacle. We could reverse the coordinates. We’re not going to. Get it right or get out of our world.
How can we test this? I think, contrary to my original plan, we have to return a result. Otherwise we’d have to search the whole world to be sure we didn’t place anything. Or, heck, we could do that. Let’s do.
_:test("erroneous obstacle calls", function()
local world = World(5,5) -- -2 to 2 thanks guys
world:createObstacle(1,1,2,2)
checkWorldEmpty("not top left")
end)
I’m imagining a function checkWorldEmpty
. A test will show me that I need to write it:
11: erroneous obstacle calls -- TestWorld:127: attempt to call a nil value (global 'checkWorldEmpty')
So I write it:
function checkWorldEmpty(world,message)
for x = world:left(),world:right() do
for y = world:bottom(),world:top() do
_:expect(world:factAt(x,y),message).is(nil)
end
end
end
Writing it tells me that it needs to be passed the world, and that the world needs to be able to report its dimensions to me. So, with no further ado:
_:test("erroneous obstacle calls", function()
local world = World(5,5) -- -2 to 2 thanks guys
world:createObstacle(1,1,2,2)
checkWorldEmpty(world, "not top left")
end)
And … (grr) …
function World:bottom()
return -(self._height-1)/2
end
function World:top()
return (self._height-1)/2
end
function World:left()
return -(self._width-1)/2
end
function World:right()
return (self._width-1)/2
end
Wouldn’t 0 and width have been so much nicer? Yes. Anyway, I expect this test to fail now, a lot … but it passes. Why? Well, because for loops work that way and my test was too easy. No problem, I’ll make it harder.
Or will I … for loops do work that way and if we can’t rely on that, what can we rely on? I’ll add some beyond range tests that should be stopped. One at a time of course:
_:test("erroneous obstacle calls", function()
local world = World(5,5) -- -2 to 2 thanks guys
world:createObstacle(1,1,2,2)
checkWorldEmpty(world, "not top left")
world = World(5,5)
world:createObstacle(-5,6, 5,-6)
checkWorldEmpty(world, "outside limits")
end)
Test expecting lots of errors, like 25 or so. And yes, I get 20+ errors saying this:
11: erroneous obstacle calls outside limits --
Actual: O,
Expected: nil
Basically I flood filled the world with obstacles.
I’m getting uncomfortable with this silent failure that I’m about to implement. Makes me really want to have some kind of indication that the obstacle creation didn’t happen. But for now, let’s make it work.
function World:createObstacle(left, top, right, bottom)
if left > right or top < bottom then return end
if left < self:left() or right>self:right() or
top>self:top() or bottom<self:bottom() then return end
for x = left,right do
for y = bottom,top do
self:addObstacle(x,y)
end
end
end
You’ll note that I decided to check the top<bottom and such, because I feel better about that than relying on the for loop rules. Now I am actually quite confident that this works. I don’t even want another test. YMMV and if you can think of one that will break our code, let me know and we’ll add it. This Is The Way.
We can commit: addObstacle takes no action if parameters are wrong.
Now of course we are green, so we can refactor. And we should: Look at this:
function World:createObstacle(left, top, right, bottom)
if left > right or top < bottom then return end
if left < self:left() or right>self:right() or
top>self:top() or bottom<self:bottom() then return end
for x = left,right do
for y = bottom,top do
self:addObstacle(x,y)
end
end
end
A method should do a thing, or it should call other methods. This method does two or three things depending on how we count. Let’s refactor:
function World:createObstacle(left, top, right, bottom)
if left > right or top < bottom then return end
if left < self:left() or right>self:right() or
top>self:top() or bottom<self:bottom() then return end
self:addObstacles(left, top, right, bottom)
end
function World:addObstacles(left, top, right, bottom)
for x = left,right do
for y = bottom,top do
self:addObstacle(x,y)
end
end
end
Test. Still good. Code slightly better. Now refactor again:
function World:createObstacle(left, top, right, bottom)
if not self:validRectangle(left,top,right,bottom) then return end
self:addObstacles(left, top, right, bottom)
end
function World:validRectangle(left,top,right,bottom)
if left > right or top < bottom then return false end
if left < self:left() or right>self:right() or
top>self:top() or bottom<self:bottom() then return false end
return true
end
This was a bit of a step. I decided to call the method valid
and that required me to put in a not
. Tests are good. Code could be better.
The early exit guard clause makes much less sense now, just guarding one line so we do this:
function World:createObstacle(left, top, right, bottom)
if self:validRectangle(left,top,right,bottom) then
self:addObstacles(left, top, right, bottom)
end
end
Better. Tests run. Now this:
function World:validRectangle(left,top,right,bottom)
if left > right or top < bottom then return false end
if left < self:left() or right>self:right() or
top>self:top() or bottom<self:bottom() then return false end
return true
end
We can do this by applying a couple of logic rules:
function World:validRectangle(left,top,right,bottom)
return left<right and top>bottom and
left>=self:left() and right<=self:right() and
top<=self:top() and bottom>=self:bottom()
end
Note that we inverted some < to >= as one does.
But you know what? There’s some weird logic going on there. The code above relies on knowing that left < right, so that it only has to check for left being too low or right being too high. Similarly for top and bottom.
I don’t like that dependency on the code. The code is coupled in some rather odd way. Let’s re-express the condition. First by intention:
function World:validRectangle(left,top,right,bottom)
return inOrder(self:left(),left,right,self:right()) and
inOrder(self:bottom(),bottom, top, self:top())
end
This, it seems to me, expresses the condition better. It looks like the picture:
We can implement that inOrder
easily enough:
local function inOrder(a,b,c,d)
return a<=b and b<=c and c<=d
end
Hm, I rather like that. Let’s look at them together:
function World:validRectangle(left,top,right,bottom)
return inOrder(self:left(),left,right,self:right()) and
inOrder(self:bottom(),bottom, top, self:top())
end
local function inOrder(a,b,c,d)
return a<=b and b<=c and c<=d
end
I do like that. I’d like it better if the method were named something like isValidRectangle
. Just validRectangle
suggests that we’re going to return a valid rectangle. The is
connotes testing. In Ruby we might have said validRectangle?
but Lua doesn’t allow that. Pity. Anyway:
function World:createObstacle(left, top, right, bottom)
if self:isValidRectangle(left,top,right,bottom) then
self:addObstacles(left, top, right, bottom)
end
end
function World:isValidRectangle(left,top,right,bottom)
return inOrder(self:left(),left,right,self:right()) and
inOrder(self:bottom(),bottom, top, self:top())
end
local function inOrder(a,b,c,d)
return a<=b and b<=c and c<=d
end
Tests run. Commit: refactor to use inOrder
function.
I should mention that, since I made inOrder
local to the tab, it has to occur before isValidRectangle
for Lua to find it. I showed it below because I find that more expressive. I am reluctant, at this point, to make the function any more global, even though it is rather handy.
Frequent readers may remember my functional programming project, which allows things like filter, map, and reduce. I could imagine using that here but not today, Satan, we’ll keep things simple for now.
We could keep inOrder
even more private by putting its definition inside isValidRectangle
but that’s a bit harder to read:
function World:isValidRectangle(left,top,right,bottom)
local function inOrder(a,b,c,d)
return a<=b and b<=c and c<=d
end
return inOrder(self:left(),left,right,self:right()) and
inOrder(self:bottom(),bottom, top, self:top())
end
It’s probably better in that it keeps the function more private at least for now. We could do this, perhaps:
function World:isValidRectangle(left,top,right,bottom)
local inOrder = function(a,b,c,d) return a<=b and b<=c and c<=d end
return inOrder(self:left(),left,right,self:right()) and
inOrder(self:bottom(),bottom, top, self:top())
end
I don’t like that any better and it looks bad in the article. Keep it inside, longhand. Commit: move local function inorder inside isValidRectangle.
It just turned 1200 hours. Let’s call it a morning and sum up.
Summary
We thought a bit about walking skeletons (scary) and how we decide what ours should be. There’s no right answer, what matters to me is to get a program that reminds the stakeholders of what they asked for, as soon as I can. My fear is that unhappy stakeholders will withdraw my funding, and my Kitty will starve.
It’s good to think about the initial decisions, even when we implement that initial decision with very little code.
We thought about what we’ve learned about the spec, and about how it might impact what we’re doing. We didn’t make any specific decisions, we’re just kind of refreshing the boundaries in our mind.
Then we provided a function that pretty well matches the spec, to place obstacles into the world (I almost said “dungeon” … 320 articles and counting). We made that function robust in that it does nothing at all if its parameters aren’t right, and we went through quite a few iterations of expressing the parameter checking until it seemed clear and neat. (At least to me. My world, my idea of clarity and neatness. Yours may vary.)
You may have noticed an opportunity for more “generality”: we know we’re going to place other things in the world, notably pits, and they will be placed pretty much the same way. We might have said to ourselves, we’re gonna need to pass in what kind of thing all the way down so let’s generalize it now.
I prefer not to do that. My reasons are two: first, I believe that too often when I do that sort of thing, I create code for things that never happen. So waiting ensures that I only add generality where it is truly valuable. Second, I enjoy deferring decisions beyond where one’s intuition might call for them, because when we discover later that something isn’t as general as we might like, it’s illuminating to see how easy it usually is to do it then.
And of course, it’s also illuminating when it’s hard. You get to laugh at me, which is good for your soul, and doesn’t bother mine at all. Bottom line, I like to defer generality past the first time or two that I think of it. I can generally find it in the code and refactor to the more general case. I’m sure that will happen here.
For today, we’ve done enough. See you next time!
-
Pronounced “Coburn”, in the Scottish fashion. ↩