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:

passing the obstacles

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.