Robot 12
Let’s see about helping the robot understand the new ‘look’ tables. I feel sure that I’m going to want an object up in this thing. The outcome may surprise you.
Tests indicate that we’re creating an array of dictionary thingies that represent the “look” findings of the World. A bit of TDD should help us tuck that information away in our robot’s knowledge. Then we “just” have to wire them together and we’ll have robot memory.
I don’t feel too terrible about creating those dictionaries, but truth be told, I think they’d be better as objects. We’ll see.
I have a bit of time this afternoon, so here goes:
Understanding the Look Dictionary
We’re going to see an array of zero or more dictionaries with a direction, step, and type. I am slightly irritated by the change from “O” to “OBSTACLE” but we’ll deal with it as we need to.
Let’s write a robot test.
_:test("Robot understands new 'look'", function()
local world = World()
local robot = Robot("Louie", world)
local look = { direction="N", type="OBSTACLE", distance=3 }
robot:addLook(look)
_:expect(robot:factAt(0,3)).is("OBSTACLE")
end)
That should be enough to get something going. Test will fail on addLook.
6: Robot understands new 'look' -- TestRobot:77: attempt to call a nil value (method 'addLook')
Implement:
function Robot:addLook(lookDict)
local steps = lookDict.distance
local item = lookDict.type
local dir = lookDict.direction
local x,y
if dir == "N" then
x,y = 0,steps
elseif dir == "S" then
x,y = 0,-steps
elseif dir == "E" then
x,y = steps,0
else
x,y = -steps,0
end
self:addFactAt(item,x,y)
end
function Robot:addFactAt(item,x,y)
self.knowledge:addFactAt(item,x,y)
end
I think I’ve done the whole job here. Code is nasty, but I think it’ll work. I’m not loving that I keep “OBSTACLE” instead of “O”, as my display likes O. Let’s change the test and code:
function Robot:addLook(lookDict)
local steps = lookDict.distance
local item = lookDict.type:sub(1,1)
local dir = lookDict.direction
local x,y
if dir == "N" then
x,y = 0,steps
elseif dir == "S" then
x,y = 0,-steps
elseif dir == "E" then
x,y = steps,0
else
x,y = -steps,0
end
self:addFactAt(item,x,y)
end
Changed test runs.
I think I’d like to change the scan
to use my new function, but first let’s commit this code, as it is safe to do so. Commit: Robot:addLook interprets and adds “look” dictionary.
Now let’s wire up the scan:
function Robot:scan()
local looks = self._world:look()
for _,look in ipairs(looks) do
self:addLook(look)
end
end
This breaks some tests, but displays what I expect on the first scan. (And on the others. Now, when I move, the picture remembers what was already there. The current version isn’t taking motion into account though.)
The old scan method passes in the robot name and world does this:
function World:scan(robotName)
local robot = self._robots[robotName]
local accumulatedKnowledge = Knowledge()
if robot then
local lens = self._knowledge:newLens(robot._x, robot._y)
self:scanNSEWinto(accumulatedKnowledge, lens)
end
return accumulatedKnowledge
end
If we’re passed a robot name we use a lens equal to the robot’s position. I need to do similarly in look.
function Robot:scan()
local looks = self._world:look(self._name)
for _,look in ipairs(looks) do
self:addLook(look)
end
end
function World:look(robotName)
local result = {}
for _,dir in ipairs({"north","south","east","west"}) do
self:lookInDirection(robotName, dir, result)
end
return result
end
function World:lookInDirection(robotName, direction, result)
local nsew= {east="E", west="W", north="N", south="S"}
local directions = {
east= {mx= 1,my= 0},
west= {mx=-1,my= 0},
north={mx= 0,my= 1},
south={mx= 0,my=-1}
}
self:lookMxMy(robotName, nsew[direction], directions[direction], result)
end
function World:lookMxMy(robotName, tag, dir, result)
local dx,dy = self:getRobotOffset(robotName)
print(dx,dy)
for xy = 1,5 do
local fact = self:factAt(dir.mx*xy+dx, dir.my*xy+dy)
if fact then
table.insert(result, self:lookPacket(xy,tag,fact))
end
end
end
This actually works. I’ll make a movie of it:
You may recall that we create our test world with an obstacle of length 4 on the left and a pit of length 3 on the right. So as we move forward in the video, we see that we remember the objects we have gone past and when we get beyond the pit the scan no longer shows it, and beyond the obstacle ditto.
We have a better demo!
Commit: Robot scan now uses World look, dictionary return, and remembers prior results.
The hookup is a bit naff. In particular I don’t like how we wedge in the dx and dy at the last minute. I think we can probably use a lens to accomplish the same thing, and surely we should do so.
Let’s see how awful the code is—or isn’t:
Look All the Way
function Robot:scan()
local looks = self._world:look(self._name)
for _,look in ipairs(looks) do
self:addLook(look)
end
end
This seems nearly good. However:
function Robot:addLook(lookDict)
local steps = lookDict.distance
local item = lookDict.type:sub(1,1)
local dir = lookDict.direction
local x,y
if dir == "N" then
x,y = 0,steps
elseif dir == "S" then
x,y = 0,-steps
elseif dir == "E" then
x,y = steps,0
else
x,y = -steps,0
end
self:addFactAt(item,x,y)
end
This is not so fine. We’d like to improve that.
Over in World:
function World:look(robotName)
local result = {}
for _,dir in ipairs({"north","south","east","west"}) do
self:lookInDirection(robotName, dir, result)
end
return result
end
function World:lookInDirection(robotName, direction, result)
local nsew= {east="E", west="W", north="N", south="S"}
local directions = {
east= {mx= 1,my= 0},
west= {mx=-1,my= 0},
north={mx= 0,my= 1},
south={mx= 0,my=-1}
}
self:lookMxMy(robotName, nsew[direction], directions[direction], result)
end
function World:lookMxMy(robotName, tag, dir, result)
local dx,dy = self:getRobotOffset(robotName)
for xy = 1,5 do
local fact = self:factAt(dir.mx*xy+dx, dir.my*xy+dy)
if fact then
table.insert(result, self:lookPacket(xy,tag,fact))
end
end
end
function World:getRobotOffset(robotName)
local robot = self._robots[robotName]
if not robot then return 0,0 end
return robot._x, robot._y
end
function World:lookPacket(xy, tag, fact)
local names = { P="PIT", O="OBSTACLE" }
return {
direction=tag,
type=names[fact],
distance=xy
}
end
This is a bit less than ideal. Is it good enough to use in the demo? I’m going to argue that it is.
However, there are some broken tests. I shouldn’t even have committed.
1: Robot updates knowledge on move -- TestRobot:101: attempt to index a nil value (field 'type')
2: Scan on all four sides -- TestRobot:101: attempt to index a nil value (field 'type')
That error is in the new addLook:
function Robot:addLook(lookDict)
local steps = lookDict.distance
local item = lookDict.type:sub(1,1)
local dir = lookDict.direction
local x,y
...
It’s the call to sub
. Oh, I know, I bet it is a mismatch between obstacle “O” and “OBSTACLE”. The setup is:
function World:test1()
local world = World()
world:addFactAt("fact",3,0)
world:addFactAt("not visible on first scan", 1,3)
return world
end
Let’s, instead of just converting the types, retain any that we don’t know how to convert:
function World:lookPacket(xy, tag, fact)
local names = { P="PIT", O="OBSTACLE" }
return {
direction=tag,
type=names[fact],
distance=xy
}
end
We’ll return the fact if we don’t find a lookup:
function World:lookPacket(xy, tag, fact)
local names = { P="PIT", O="OBSTACLE" }
return {
direction=tag,
type=names[fact] or fact,
distance=xy
}
end
I think this ought to make the tests run. Well, almost: we only get the first letter. Bummer. Fix the tests. Or …
No. This is good, and I think I know how to do the look well. I might even build a copy or show the movie. But this code isn’t fit to be in the repo.
Undo that last commit, and revert. We’ll do it again next time, better.
Let’s sum up. I feel good about this.
Summary
I could have kept this code and cleaned it up tomorrow. It wouldn’t take long, and it would look great in the demo. I’d be proud of how it looks on screen.
And I’m proud that in just an hour, including all this writing, I got it to work.
But I’m not proud of how it works. The World side is still a bit shaky, and the injection of the dx,dy idea is particularly ad hoc. The linkage is OK, but the addLook
code is also more than a little ad hoc.
I can do better, and I will do. I’ve done the right thing: I learned how to do it, and even made a video of it working. And I refused to put code in the repo that I couldn’t be at least reasonably proud of.
I feel good about this.