Try again. Fail again. Fail better. Been down so long, it looks like up to me. Summary: Much better.

Beckett’s quote, and Fariña’s, may seem a bit like downers, but as software developers, we encounter a lot of down, a lot of failure, and it’s really important, in my view, to look it in the eye, take it in, and then set it aside. We have the opportunity, should we decide to accept it, to keep our failures generally quite small, and to learn from each one.

We keep them small by taking small steps, each step supported by tests1 showing us what the software is doing, and providing confidence that it’s doing what we intend. We learn from them by thinking about what has happened, what we might have done differently, deciding to change how we work, and by improving our tools to help us do better.

Was yesterday’s Robot 12 article describing a failure, or a success? It depends. I elected to roll back my changes after a bit of soul-searching. The code just wasn’t up to my standards, and I didn’t feel right about demonstrating a feature that wasn’t in decent order. I don’t expect perfection, but I know the difference between good work and sloppy work, and what I had was still sloppy.

So I “failed” to deliver on something that had not even been promised. And I learned a lot about how to do the thing, and I even gained a little bit of pleasure from doing the right thing. When I walk away from the computer confused and sad because I just plain do not understand what went wrong, that feels like failure. When I walk away knowing more than I used to know, that feels like success.

Of course, the best successes leave a nice feature in the program. We’ll work toward that today.

The New Look Feature

The feature can be looked at from a few angles:

On Screen
When we move and scan and move and scan, the radar builds up a picture of what is around us, integrating old scans and new ones into a picture of everything we’ve scanned.
Format
We need an information format different from our first approach. The new scheme is like an array of dictionaries, with the odd rule that North means “ahead of you, whatever direction you’re pointing”.
World
The world has code to create the new format, but it is not keeping track of motion very nicely, and is not tracking the turn direction of the robot at all. It would be nice if we could use the Lens for this. This code is not yet used in the game.
Robot
The robot has been rolled back, which was a bit extreme, so it needs to have its scanning function implemented in terms of the new World scheme.

Let’s plan.

Tentative Plan

Every plan is a tentative plan, unless for some reason you believe that you can predict the future perfectly, in which case you should be playing the Lottery, not reading this. I’ve got some ideas:

One idea will be to clean up the World code a bit, and then start on Robot. The World code has reasonable tests, so we can clean it up pretty well.

Another would be to clean up the World, and to modify the implementation to use a Lens to view its Knowledge from the viewpoint of the Robot. (Right now it is adjusting the coordinates manually.) That would be useful later on, when the Lens scheme fully supports turning. (Right now, there is an experimental RotatingLens that needs integration with Lens.)

I think I’ll go for using the Lens now. It seems like the right design, and since the code needs work anyway, now’s good. No rush now that the demo is over. I think perhaps I was rushing yesterday, though I didn’t really feel it.

Lenserating the World

Lensizing? Lensing? I don’t know. Anyone remember Kimball Kinnison? But I digress. Here’s the look code in world now:

function World:look()
    local result = {}
    for _,dir in ipairs({"north","south","east","west"}) do
        self:lookInDirection(dir, result)
    end
    return result
end

function World:lookInDirection(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(nsew[direction], directions[direction], result)
end

function World:lookMxMy(tag, dir, result)
    for xy = 1,5 do
        local fact = self:factAt(dir.mx*xy, dir.my*xy)
        if fact then
            table.insert(result, self:lookPacket(xy,tag,fact))
        end
    end
end

function World:lookPacket(xy, tag, fact)
    local names = { P="PIT", O="OBSTACLE" }
    return {
        direction=tag,
        type=names[fact],
        distance=xy
    }
end

Oh, that reminds me: I think that the Look Packet should be an object, not a raw dictionary. No real reason other than general design sense, but I bet it winds up with a useful method or two. I’ll address that late on in today’s work … or however many sessions it takes to get there. Before moving to Robot, anyway.

Now I believe that the code above no longer handles a move at all. I recall adding in a dx,dy component yesterday. I had thought of putting in a Lens but I couldn’t see a clean way offhand. Rushing. Very interesting. I probably was, but wasn’t really aware of it.

What makes sense? Our look function is really only looking at the axes around World(0,0). It has no notion of offsetting for robot location at all.

Let’s review scan, which does involve a lens:

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

function World:scanNSEWinto(accK, lens)
    local direction = {
        east= {mx= 1,my= 0},
        west= {mx=-1,my= 0},
        north={mx= 0,my= 1},
        south={mx= 0,my=-1}
    }
    self:scanInDirection(accK,lens, direction.north)
    self:scanInDirection(accK,lens, direction.south)
    self:scanInDirection(accK,lens, direction.east)
    self:scanInDirection(accK,lens, direction.west)
end

function World:scanInDirection(accK,lens, direction)
    for xy = 1,5 do
        self:scanFactInto(accK,lens, direction.mx*xy, direction.my*xy)
    end
end

function World:scanFactInto(accumulatedKnowledge,lens, x,y)
    local factContent = lens:factAt(x,y)
    if factContent then
        accumulatedKnowledge:addFactAt(factContent,x,y)
    end
end

When I did the look, I reviewed ``scan but shaped look differently. I did look` separately because I didn’t want to break the existing capability of scanning and moving in the demo. Today let’s try something different.

Something Different

One advantage to taking a different angle is that we’re less likely to make the same mistakes we made last time. We get to make nice new mistakes. Whee!

Let’s create a new object, LookPacket, with direction, distance, and type, and get World to create them, and prepare Robot to accept them. Once we’ve done that, we should be quite close to bridging the gap with the new look.

And let’s start—this is a change—with Robot, using the new thing. We’ll TDD the object, though it is trivial, and then teach Robot to understand it.

        _:test("LookPacket", function()
            local packet = LookPacket("N", 5, "O")
            _:expect(packet:direction()).is("N")
            _:expect(packet:distance()).is(5)
            _:expect(packet:type()).is("O")
        end)

Yes, this is almost ludicrous, but now we’ve paid the price of setting up a framework for this object, so that adding tests later will be a bit less of a big bite. I think it may pay off.

Anyway, it is the work of moments to make this test run:

LookPacket = class()

function LookPacket:init(direction, distance, type)
    self._direction = direction
    self._distance = distance
    self._type = type
end

function LookPacket:direction()
    return self._direction
end

function LookPacket:distance()
    return self._distance
end

function LookPacket:type()
    return self._type
end

No surprises there. Now let’s see if we can write some tests helping the Robot use these things. I think we’ll encounter a problem fairly early on.

We have a related 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("O")
        end)

Neat. We can change that to this:

        _:test("Robot understands new 'look'", function()
            local world = World()
            local robot = Robot("Louie", world)
            local look = LookPacket("N", 3, "OBSTACLE")
            robot:addLook(look)
            _:expect(robot:factAt(0,3)).is("O")
        end)

This should fail nicely.

6: Robot understands new 'look' -- TestRobot:101: attempt to index a function value (field 'type')

That leads us right to the problem spot:

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

Yucch, that’s not exactly sweet is it?

But we can readily make it work:

function Robot:addLook(lookPacket)
    local steps = lookPacket:distance()
    local item = lookPacket:type():sub(1,1)
    local dir = lookPacket: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

I expect success. Yes, test is green. Now what can we do about not hating that if nest so much? Well, we can factor it out:

function Robot:addLook(lookPacket)
    local steps = lookPacket:distance()
    local item = lookPacket:type():sub(1,1)
    local dir = lookPacket:direction()
    local x,y = self:convertToXY(dir,steps)
    self:addFactAt(item,x,y)
end

function Robot:convertToXY(dir,steps)
    if dir == "N" then
        return 0,steps
    elseif dir == "S" then
        return 0,-steps
    elseif dir == "E" then
        return steps,0
    else
        return -steps,0
    end
end

OK, that’s got all the nasty isolated. Let’s convert conditional to data:

function Robot:addLook(lookPacket)
    local steps = lookPacket:distance()
    local item = lookPacket:type():sub(1,1)
    local dir = lookPacket:direction()
    local x,y = self:convertToXY(dir,steps)
    self:addFactAt(item,x,y)
end

function Robot:convertToXY(dir,steps)
    local convert = { N={0,1}, S={0,-1}, E={1,0}, W={-1,0} }
    local mul = convert[dir]
    return steps*mul[1], steps*mul[2]
end

OK, I call that nice. Tests are green. Commit: Robot understand new LookPacket.

Are you wondering about whether that is enough testing? Certainly we will now do something bad if we get a bad packet. Let’s write a test for that.

        _:test("Robot ignores bad packet direction", function()
            local world = World()
            local robot = Robot("Louie", world)
            local look = LookPacket("Q", 3, "OBSTACLE")
            robot:addLook(look)
            _:expect(robot.knowledge:factCount()).is(0)
        end)

We want nothing to be accepted on a bad packet. As things stand, this code will crash.

7: Robot ignores bad packet direction -- TestRobot:118: attempt to index a nil value (local 'mul')

Let’s return nil,nil as the x and y.

function Robot:convertToXY(dir,steps)
    local convert = { N={0,1}, S={0,-1}, E={1,0}, W={-1,0} }
    local mul = convert[dir]
    if not mul then return nil,nil end
    return steps*mul[1], steps*mul[2]
end

This probably still crashes. I’m not sure what addFactAt does on nils, but I think we’re going to find out and change it.

7: Robot ignores bad packet direction  -- 
Actual: 1, 
Expected: 0

Curious. Let’s have a look. It comes down to this:

function Knowledge:privateaddFactAt(aFact)
    table.insert(self.facts, aFact)
end

Looks like we create a Fact instance with nil x and y and save it in our array. I’m not sure quite how we got past all the adding.

Ah. The adding takes place in Lens, and we have not moved, so we do not have a Lens. Let’s change the test.

        _:test("Robot ignores bad packet direction", function()
            local world = World()
            local robot = Robot("Louie", world)
            robot:move(0,1)
            local look = LookPacket("Q", 3, "OBSTACLE")
            robot:addLook(look)
            _:expect(robot.knowledge:factCount()).is(0)
        end)

Now it fails as I expected:

7: Robot ignores bad packet direction -- TestKnowledge:98: attempt to perform arithmetic on a nil value (local 'x')

OK, what do we want here? We want this thing ignored. And we’d like to do it without everyone checking all over for nils. I don’t see a good way. I’ll do this, which is OK but not lovely:

function Robot:addFactAt(item,x,y)
    if not x or not y then return end
    self.knowledge:addFactAt(item,x,y)
end

This finds a flaw:

7: Robot ignores bad packet direction -- TestRobot:87: attempt to call a nil value (method 'factCount')

Lens doesn’t understand factCount:

function Lens:factCount()
    return self._knowledge:factCount()
end

Green: Commit: Robot ignores LookPackets with bad direction.. Too much?

I guess it’s righteous. In my own code I would demand that everyone can be trusted. But the LookPackets come from “outside”, across the network. So: not too much.

Time to raise head and look around.

Where Are We?

Right. Robot will understand LookPackets if we can get any to it. We’re on a clean commit. Let’s TDD World to the point where it can create a LookPacket. Then we’ll see about plugging it in and bridging the gap.

As soon as I start to write the test, I see the old “look” tests and they stop me in my tracks. After a little reflection, I decide to ignore them and look into World’s scan (not look) code and see where we have the right info to make a LookPacket.

function World:scanInDirection(accK,lens, direction)
    for xy = 1,5 do
        self:scanFactInto(accK,lens, direction.mx*xy, direction.my*xy)
    end
end

function World:scanFactInto(accumulatedKnowledge,lens, x,y)
    local factContent = lens:factAt(x,y)
    if factContent then
        accumulatedKnowledge:addFactAt(factContent,x,y)
    end
end

If we assume facts not in evidence, that the lens we pass in will at some future time include rotation, then the scanFactInto will have just the info we need, albeit not in a handy form.

Let’s TDD a new method like scanFactInto. I’ll call it newScanFactInto … Writing the test I change my mind about how far to go. Here’s my test:

        _:test("World can create LookPacket from scan", function()
            local world = World(25,25)
            world:createPit(0,3,0,3) -- N 3
            local packet = world:createPacketFor(0,3)
            _:expect(packet:direction()).is("N")
            _:expect(packet:steps()).is(3)
            _:expect(packet:type()).is("Pit")
        end)

I’m positing that createPacketFor(x,y) will return nil or a LookPacket. Test will fail looking for that.

18: World can create LookPacket from scan -- TestWorld:322: attempt to call a nil value (method 'createPacketFor')

Yass. Now it’s a SMOP. I immediately see that I need to pass in a lens or knowledge:

        _:test("World can create LookPacket from scan", function()
            local world = World(25,25)
            world:createPit(0,3,0,3) -- N 3
            local lens = world._knowledge:newLens(0,0)
            local packet = world:createPacketFor(lens, 0,3)
            _:expect(packet:direction()).is("N")
            _:expect(packet:distance()).is(3)
            _:expect(packet:type()).is("Pit")
        end)

And …

function World:createPacketFor(lens,x,y)
    local content = lens:factAt(x,y)
    if not content then return nil end
    local dir = self:directionTo(x,y)
    local steps = math.max(math.abs(x), math.abs(y))
    return LookPacket(dir, steps, content)
end

function World:directionTo(x,y)
    if x > 0 then return "E" end
    if x < 0 then return "W" end
    if y > 0 then return "N" end
    return "S"
end

Test nearly works:

18: World can create LookPacket from scan  -- 
Actual: P, 
Expected: Pit

Well, we need to decide whether we’re using long or short names for the obstacles. The packet doesn’t care. The spec says we have long words. We should settle on that and truncate them in our display or whatever we want done there.

For now, I’m going to go with what the system does now, which is just the single letters stored in World.

        _:test("World can create LookPacket from scan", function()
            local world = World(25,25)
            world:createPit(0,3,0,3) -- N 3
            local lens = world._knowledge:newLens(0,0)
            local packet = world:createPacketFor(lens, 0,3)
            _:expect(packet:direction()).is("N")
            _:expect(packet:distance()).is(3)
            _:expect(packet:type()).is("P")
        end)

Test passes. Commit … no. I coded past the test. Let’s complete the testing. I can copy from the old look tests.

        _:test("World can create LookPacket from scan", function()
            local world = World(25,25)
            world:createPit(0,3,0,3) -- N 3
            world:createPit(0,-4,0,-4) -- S 4
            world:createObstacle(2,0,2,0) -- E 2
            world:createObstacle(-5,0,-5,0) -- W 5
            local lens = world._knowledge:newLens(0,0)
            local packet = world:createPacketFor(lens, 0,3)
            _:expect(packet:direction()).is("N")
            _:expect(packet:distance()).is(3)
            _:expect(packet:type()).is("P")
            local packet = world:createPacketFor(lens, 0,-4)
            _:expect(packet:direction()).is("S")
            _:expect(packet:distance()).is(4)
            _:expect(packet:type()).is("P")
            local packet = world:createPacketFor(lens, 2,0)
            _:expect(packet:direction()).is("E")
            _:expect(packet:distance()).is(2)
            _:expect(packet:type()).is("O")
            local packet = world:createPacketFor(lens, -5,0)
            _:expect(packet:direction()).is("W")
            _:expect(packet:distance()).is(5)
            _:expect(packet:type()).is("O")
        end)

OK, now commit: World:createPacketFor(lens,x,y) returns correct LookPackets.

Time for a little break, I’ve been here about two hours. You can take a break, too, if you like. But you knew that.

1415

OK, I might get back to it. Let’s review what we have. k

We have a new little object, LookPacket, that contains the direction, distance, and object found during a scan. It expresses things as N, S, E, W and a distance.

We have a new function in World that can create a LookPacket relative to the current robot’s lens, which means that the packets that come out are “forward = north” as the spec calls for.

We have a new function in Robot that can create a fact from a LookPacket.

Let’s review the code that creates and uses the packets:

function World:createPacketFor(lens,x,y)
    local content = lens:factAt(x,y)
    if not content then return nil end
    local dir = self:directionTo(x,y)
    local steps = math.max(math.abs(x), math.abs(y))
    return LookPacket(dir, steps, content)
end

function World:directionTo(x,y)
    if x > 0 then return "E" end
    if x < 0 then return "W" end
    if y > 0 then return "N" end
    return "S"
end

That’s pretty simple. It returns nil when it finds nothing.

And in Robot:

function Robot:addLook(lookPacket)
    local steps = lookPacket:distance()
    local item = lookPacket:type():sub(1,1)
    local dir = lookPacket:direction()
    local x,y = self:convertToXY(dir,steps)
    self:addFactAt(item,x,y)
end

It would seem that we ought to be able to plug these two functions in and hook up a better scan.

World-Side

In World, we have this today:

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

function World:scanNSEWinto(accK, lens)
    local direction = {
        east= {mx= 1,my= 0},
        west= {mx=-1,my= 0},
        north={mx= 0,my= 1},
        south={mx= 0,my=-1}
    }
    self:scanInDirection(accK,lens, direction.north)
    self:scanInDirection(accK,lens, direction.south)
    self:scanInDirection(accK,lens, direction.east)
    self:scanInDirection(accK,lens, direction.west)
end

function World:scanInDirection(accK,lens, direction)
    for xy = 1,5 do
        self:scanFactInto(accK,lens, direction.mx*xy, direction.my*xy)
    end
end

function World:scanFactInto(accumulatedKnowledge,lens, x,y)
    local factContent = lens:factAt(x,y)
    if factContent then
        accumulatedKnowledge:addFactAt(factContent,x,y)
    end
end

The rules are that the look operation—its real name—returns an array of information that we represent in a LookPacket. So if we pass in a vanilla array as accumulatedKnowledge, and insert into it, we should come up with an array of LookPackets.

Let’s just do this and wire it up based on what happens.

function World:scan(robotName)
    local robot = self._robots[robotName]
    local accumulatedKnowledge = {}
    if robot then
        local lens = self._knowledge:newLens(robot._x, robot._y)
        self:scanNSEWinto(accumulatedKnowledge, lens)
    end
    return accumulatedKnowledge
end

Now the array will pass down to here:

function World:scanFactInto(accumulatedKnowledge,lens, x,y)
    local factContent = lens:factAt(x,y)
    if factContent then
        accumulatedKnowledge:addFactAt(factContent,x,y)
    end
end

We can do this:

function World:scanFactInto(accK, lens, x,y)
    table.insert(accK, self:createPacketFor(lens,x,y))
end

If we insert a nil into the table, it has no effect, so this should be good to go. Let’s see what breaks on the other end.

All kinds of robot tests, no surprise, all pretty much saying this:

1: Robot updates knowledge on move -- TestRobot:129: attempt to call a nil value (method 'factAt')

We need to change what Robot does on scan:

function Robot:scan()
    self.knowledge = self._world:scan(self._name)
end

We’ll get an array back, which will contain LookPackets, which we can add to our own knowledge:

function Robot:scan()
    local packets = self._world:scan(self._name)
    for i,packet in ipairs(packets) do
        self:addLook(packet)
    end
end

This breaks many tests, but on the map we get this:

4 obstacles, 3 pits

This seems to be working perfectly. The map now remembers what we’ve seen, and our motion is reflected in what the World sends back to us. Let’s see about those tests. Some will probably want to be removed.

1: Robot updates knowledge on move  -- 
Actual: f, 
Expected: fact
1: Robot updates knowledge on move after second scan -- 
Actual: n, 
Expected: not visible on first scan
2: Scan on all four sides  -- 
Actual: r, 
Expected: right
2: Scan on all four sides  -- 
Actual: l, 
Expected: left
2: Scan on all four sides  -- 
Actual: u, 
Expected: up
2: Scan on all four sides  -- 
Actual: d, 
Expected: down

These are clearly just fine, except that we are truncating the string on return. That should probably be done elsewhere. Where is that, anyway?

function Robot:addLook(lookPacket)
    local steps = lookPacket:distance()
    local item = lookPacket:type():sub(1,1)
    local dir = lookPacket:direction()
    local x,y = self:convertToXY(dir,steps)
    self:addFactAt(item,x,y)
end

Let’s remove that from here and just store whatever the item is. That will mean we’ll get “OBSTACLE” and such in due time.

function Robot:addLook(lookPacket)
    local steps = lookPacket:distance()
    local item = lookPacket:type()
    local dir = lookPacket:direction()
    local x,y = self:convertToXY(dir,steps)
    self:addFactAt(item,x,y)
end

This will make the tests run and, I suspect, the display go bad.

One test newly fails:

6: Robot understands new 'look'  -- 
Actual: OBSTACLE, 
Expected: O

That one should be fixed to expect the whole word:

        _:test("Robot understands new 'look'", function()
            local world = World()
            local robot = Robot("Louie", world)
            local look = LookPacket("N", 3, "OBSTACLE")
            robot:addLook(look)
            _:expect(robot:factAt(0,3)).is("OBSTACLE")
        end)

Tests will be green. The map still works. I wonder why. Let’s check how the display works. It comes down to here:

function drawCell(x,y, size)
    local fact = CurrentRobot:factAt(x,y)
    if x == 0 and y == 0 then
        fact = "R"
    end
    local gd = GridDisplay:display(fact)
    if gd.color then
        fill(gd.color)
    else
        noFill()
    end
    rect(x*size,y*size,size,size)
    if gd.text then
        fill(0,256,0)
        text(gd.text, x*size,y*size)
    end
end

And thence to:

function GridDisplay:display(content)
    if content == "O" then
        return {color=color(0,256,0,128)}
    elseif content == "P" then
        return { color=color(0)}
    elseif content == "R" then
        return {text="R", color = color(0,256,0,48)}
    else
        return {color=color(0,256,0,48)}
    end
end

Ah. It comes down to this:

function World:createObstacle(left, top, right, bottom)
    self:createThings(left, top, right, bottom, "O")
end

So far, we are not creating those facts with the full words. When we do, something will have to give. Let me do a commit here, since we are green and have a solid display. Commit: Now using LookPacket array as communication between world and robot display.

Now let’s change the createObstacle to use the whole word:

function World:createObstacle(left, top, right, bottom)
    self:createThings(left, top, right, bottom, "OBSTACLE")
end

I think we’ll find that the display goes wonky, but I expect the tests to run. I can imagine some of them breaking on O vs OBSTACLE, but we’ll see.

Some “look” tests fail. Let’s remove all the look stuff.

That done, I get these errors:

11: rectangular obstacle  -- 
Actual: OBSTACLE, 
Expected: O
(10 of those)
14: World can create LookPacket from scan  -- 
Actual: OBSTACLE, 
Expected: O
(2 of those)
3: Set Up Game  -- 
Actual: OBSTACLE, 
Expected: O

Fixes same throughout, expect OBSTACLE. Here’s one that we’ll need to address again:

        _:test("Set Up Game", function()
            local robot = Robot:setUpGame()
            _:expect(robot:factAt(-5,0)).is(nil)
            _:expect(robot:factAt(3,0)).is(nil)
            robot:scan()
            _:expect(robot:factAt(-5,0)).is("OBSTACLE")
            _:expect(robot:factAt(3,0)).is("P")
        end)

Let’s make the other change:

        _:test("Set Up Game", function()
            local robot = Robot:setUpGame()
            _:expect(robot:factAt(-5,0)).is(nil)
            _:expect(robot:factAt(3,0)).is(nil)
            robot:scan()
            _:expect(robot:factAt(-5,0)).is("OBSTACLE")
            _:expect(robot:factAt(3,0)).is("PIT")
        end)

And …

function World:createPit(left, top, right, bottom)
    self:createThings(left, top, right, bottom, "PIT")
end

I expect green tests and bad display. Well, I had some tests for P that needed to say PIT. Now I’m green. Let’s see what the display does.

As expected, nothing comes out. Back to this:

        _:test("GridDisplay", function()
            local gd = GridDisplay()
            _:expect(gd:display(nil).color).is(color(0,256,0,48))
            _:expect(gd:display("OBSTACLE").color).is(color(0,256,0,128))
            _:expect(gd:display("PIT").color).is(color(0))
            _:expect(gd:display("R").color).is(color(0,256,0,48))
            _:expect(gd:display("R").text).is("R")
        end)

function GridDisplay:display(content)
    if content == "OBSTACLE" then
        return {color=color(0,256,0,128)}
    elseif content == "PIT" then
        return { color=color(0)}
    elseif content == "R" then
        return {text="R", color = color(0,256,0,48)}
    else
        return {color=color(0,256,0,48)}
    end
end

Picture is now good:

picture showing obstacles on left pits on right as intended

And we have out new scanning feature and the code is actually pretty decent. I feel good enough to commit it and probably to go run to the stakeholders and say “look look”. Commit: World and Robot now agree on terms OBSTACLE and PIT, not O and P.

Let’s sum up.

Summary

This almost always happens. When we do something and it either doesn’t get done or isn’t quite right, when we do it over, it goes more smoothly and generally takes less time. We wind up with a much better situation than we would if we were to debug the first day’s stuff into submission.

We’ll review the code next time. I’m sure it can be improved somewhat, and I’d like to make it a bit less sensitive to the exact values of “OBSTACLE” and such. Nothing too fancy, but something.

Overall, a good day and a nice new end-to-end capability.



  1. My friend and nemesis Michael Bolton would prefer to call them “checks”, because he prefers to think of testing as a “sapient”, thoughtful activity. He’s not wrong. But I often call them tests anyway. My house my rules.