Today’s demo day. We’re good to go, but I think we can do better. Anyway, gotta do somethin’. Anti-Spoiler: I decide not to spoil the code for the sake of maybe a feature.

We can already demonstrate more than we promised our stakeholders:

  1. Robot on the plain;
  2. Scan shows what’s NSEW of the robot;
  3. Robot can move forward (North);
  4. Robot can turn right. (over-delivered!)

There are some pretty obvious “next things” that need doing, among a huge number of total requirements. I’d select these as high priority:

  1. Robot does not remember what’s scanned;
  2. Robot can turn but can only actually move North;
  3. It’d be nice if we could turn left as well as right;

When we can resolve those issues, it’ll be possible to have the robot explore the world and build up a picture in its “mind” as to what’s there.

This morning, I’ll work on those things. I think the most valuable will be having the robot remember what it has seen. Let’s get to it.

Robot Remembers

Currently, the world has a Knowledge, containing the truth about the world, and the robot has a Knowledge, containing what the world has most recently sent it. he robot actually just keeps a Knowledge that is passed to it.

What’s supposed to happen is that the world sends a scan result to the robot, consisting of a truly bizarre JSON object, like this:

{
	"result": "OK",
	"data": {
		"objects": [
			{
				"direction": ....,
				"type": type of object
				"distance": steps
			},
			{ ... },
			...
		]
	},
	"state": { ... }
}

I hope it’s clear that in Lua we’ll represent that information in tables, some of array type and some of dictionary type, with string keys and whatever values make sense, typically strings and numbers. Basically JSON always shakes out like that. You’d almost think it was designed that way.

The “direction” up there is “N”, “S”, “E”, or “W”, with the direction “N” really meaning “ahead of the robot”, not actually true North.

So we need to create a dictionary-like thing in the world scan, and return that, instead of a Knowledge instance, to the robot, who needs to decode it.

Here we go:

New Output Format in Scan

We’re working on a fresh pull from the repo, and the plan is that we’ll commit whenever it’s safe, and certainly whenever the demo will be better. But we won’t regress.

We rarely if ever allow an intentional regression. We’d do it only for a very good reason and with stakeholder approval. Offhand, I can’t think of a good reason, unless they were to ask us to. Anyway, we’re not going to regress.

We need a new output from scan, so we need some new tests. It turns out that all the tests for World:scan are actually done through the robot. We’ll just let that be for now. Those tests should work when the scan correctly creates its output, and when the new output is integrated into the robot’s knowledge.

I’ll work on a new method, which I’ll call look, which happens to be the actual name of the message that requests a scan. (I finally got access to some requirements documents, which I am following when it seems interesting. I’m here to learn how to build what we want, not to follow any particular path.)

I started writing a complicated complete test, but decided to simplify it to get the basic shape of things in place. It looks like this:

        _:test("Look creates array of dictionaries", function()
            local world = World(25,25) -- -12 to 12
            -- left, top, right, bottom
            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 array = world:look()
            _:expect(#array).is(1)
            local n = array[1]
            _:expect(n.direction).is("N")
            _:expect(n.type).is("PIT")
            _:expect(n.distance).is(3)
        end)

Testing should fail on look.

14: Look creates array of dictionaries -- TestWorld:243: attempt to call a nil value (method 'look')

Implement look. I’m not going to play strict TDD, I’m going to code more than the minimum to get to the next failure. Whatever feels right.

function World:look()
    return {}
end

This’ll fail the count.

14: Look creates array of dictionaries  -- 
Actual: 0, 
Expected: 1
14: Look creates array of dictionaries -- TestWorld:250: attempt to index a nil value (local 'n')

Natch. Now I could do some faking, but instead I think I’ll just try to do the job.

One way to go would be to use the existing scan, which returns a Knowledge. That’s possible but seems around the horn. Another way to go would be to modify the existing scan as I need it. That would break the system and I couldn’t commit my code until it worked. A third way is to use the existing scan code to guide creation of something new. I choose that.

Scanning goes like 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

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

I think I’lll borrow the bottom part of the idea, creating lookInDirection and using it. Now the rule is that a look only returns 0 or 1 thing, so I think I can do something like this:

function World:look()
    local direction = {
        east= {mx= 1,my= 0},
        west= {mx=-1,my= 0},
        north={mx= 0,my= 1},
        south={mx= 0,my=-1}
    }
    local result = {}
    local item
    item = self:lookInDirection(direction.north, "N")
    if item then
        table.insert(result,item)
    end
    return result
end

Now I need lookInDirection. Let me stub it:

function World:lookInDirection(direction, key)
    return {
        direction="N",
        type="Pit",
        distance=3
    }
end

I expect the test to run. It doesn’t:

14: Look creates array of dictionaries  -- 
Actual: Pit, 
Expected: PIT

Man can’t even get a stub right. Fix:

function World:lookInDirection(direction, key)
    return {
        direction="N",
        type="PIT",
        distance=3
    }
end

Test runs. I could commit. Let’s talk about what just happened.

Brief Reflection

I found that I was taking too big a bite. I wasn’t really sure what part of the information should come back from lookInDirection and what part should be filled in by the calling method. I wasn’t sure how I was going to implement lookInDirection at all. I realized that I needed either to translate the “P” in Knowledge to “PIT” or change the rules on how we store things.

In short, it was too much. So I stubbed the method to return a constant result. This pattern is “Fake It Till You Make It”. It’s useful to get back to green, with some code at least roughly like we want it, and other bits not done at all. Far better than everything nearly working.

Enough reflection get back in there

So let’s review what we have:

function World:look()
    local direction = {
        east= {mx= 1,my= 0},
        west= {mx=-1,my= 0},
        north={mx= 0,my= 1},
        south={mx= 0,my=-1}
    }
    local result = {}
    local item
    item = self:lookInDirection(direction.north, "N")
    if item then
        table.insert(result,item)
    end
    return result
end

function World:lookInDirection(direction, key)
    return {
        direction="N",
        type="PIT",
        distance=3
    }
end

The first thing of note is that look doesn’t really care about the direction table. We can push that down. Let’s do:

function World:look()
    local result = {}
    local item
    item = self:lookInDirection("north")
    if item then
        table.insert(result,item)
    end
    return result
end

function World:lookInDirection(direction)
    local directions = {
        east= {mx= 1,my= 0},
        west= {mx=-1,my= 0},
        north={mx= 0,my= 1},
        south={mx= 0,my=-1}
    }
    return {
        direction="N",
        type="PIT",
        distance=3
    }
end

Tests still green. Now let’s see about actually scanning north.

I code this, pulling it out by reviewing and roughly copying the scan code. I am not proud of this but I think it might actually work. If it does, I expect to fail the test seeing P instead of PIT.

Not quite:

14: Look creates array of dictionaries -- TestWorld:103: attempt to perform arithmetic on a nil value (global 'mx')

Fix:

    local nsew= {east="E", west="W", north="N", south="S"}
    local dir = directions[direction]
    for xy = 1,5 do
        local fact = self:factAt(dir.mx*xy, dir.my*xy)
        if fact then
...

Test.

14: Look creates array of dictionaries  -- 
Actual: P, 
Expected: PIT

OK, in for a penny, I’ll fix this:

function World:lookInDirection(direction)
    local directions = {
        east= {mx= 1,my= 0},
        west= {mx=-1,my= 0},
        north={mx= 0,my= 1},
        south={mx= 0,my=-1}
    }
    local names = { P="PIT" }
    local nsew= {east="E", west="W", north="N", south="S"}
    local dir = directions[direction]
    for xy = 1,5 do
        local fact = self:factAt(dir.mx*xy, dir.my*xy)
        if fact then
            return {
                direction=nsew[direction],
                type=names[fact],
                distance=xy
            }
        end
    end
    return nil
end

I expect the test to run. It does. The code is less than lovely. But I think it is close to working on the World side.

We’re looking at a very serious question right now. Let’s take a section and address it:

Dare We Do This?

We are one hour into a two, maybe three hour session. We have some very rough code creating a properly-formatted dictionary. With only a few tweaks, essentially calling four times rather than one, we’ll have an array of results suitable for use by the robot to update its Knowledge.

But the code is rough. If we push forward, we’ll quite possibly have a much nicer demo, with memory, if not rotation.

But the code is rough. And next week, with new stories to implement, will we improve this code as it deserves, or will we say “good enough, we’ll do it later”?

The code is isolated. It’s functionally OK, or will be by the time we demo it. It’s just not very nice. It’s rough.

If we let it go … maybe nothing bad will happen. Or, maybe we’ll have occasion to work on it because of changes to the spec or a new kind of obstacle (there are “mines”, did I mention that?). And if we work on it, we’re really good disciplined folx and we’ll clean it up then.

Sure, and the Golden Gate Bridge can be yours for the low low price of $100,000 cash on the barrelhead, offer expires today, act now.

In one of our Tuesday “Friday Night Coding” Zoom meetings, Hill said “I always write it the best I know how”. I thought he was perhaps exaggerating, because he is not otherwise saintly, but it’s a darn good thing to aspire to.

OK. I’m going to do this well. Maybe not the best I know how, but at least well. To do that, I need to improve it as soon as the improvements become obvious. We’re almost there but let’s make it a bit worse before we make it better.

Next Direction

I have more proposed items in the test that I can uncomment. Let’s see:

        _:test("Look creates array of dictionaries", function()
            local world = World(25,25) -- -12 to 12
            -- left, top, right, bottom
            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 array = world:look()
            _:expect(#array).is(1)
            local n = array[1]
            _:expect(n.direction).is("N")
            _:expect(n.type).is("PIT")
            _:expect(n.distance).is(3)
        end)

Now one of the things that stopped me there was that I don’t want to specify the order of those objects in the array, so that I can’t be sure whether north is first or last.

So I’ll write four tests.

        _:test("N: Look creates array of dictionaries", function()
            local world = World(25,25) -- -12 to 12
            -- left, top, right, bottom
            world:createPit(0,3,0,3) -- N 3
            local array = world:look()
            _:expect(#array).is(1)
            local n = array[1]
            _:expect(n.direction).is("N")
            _:expect(n.type).is("PIT")
            _:expect(n.distance).is(3)
        end)
        
        _:test("S: Look creates array of dictionaries", function()
            local world = World(25,25) -- -12 to 12
            -- left, top, right, bottom
            world:createPit(0,-4,0,-4) -- S 4
            local array = world:look()
            _:expect(#array).is(1)
            local n = array[1]
            _:expect(n.direction).is("S")
            _:expect(n.type).is("PIT")
            _:expect(n.distance).is(4)
        end)
        
        _:test("E: Look creates array of dictionaries", function()
            local world = World(25,25) -- -12 to 12
            -- left, top, right, bottom
            world:createObstacle(2,0,2,0) -- E 2
            local array = world:look()
            _:expect(#array).is(1)
            local n = array[1]
            _:expect(n.direction).is("E")
            _:expect(n.type).is("OBSTACLE")
            _:expect(n.distance).is(2)
        end)
        
        _:test("W: Look creates array of dictionaries", function()
            local world = World(25,25) -- -12 to 12
            -- left, top, right, bottom
            world:createObstacle(-5,0,-5,0) -- W 5
            local array = world:look()
            _:expect(#array).is(1)
            local n = array[1]
            _:expect(n.direction).is("W")
            _:expect(n.type).is("OBSTACLE")
            _:expect(n.distance).is(5)
        end)

These will all fail. We’ll tick thru them:

15: S: Look creates array of dictionaries  -- 
Actual: 0, 
Expected: 1

Do south … no, once I get there, I do them all:

function World:look()
    local result = {}
    local item
    item = self:lookInDirection("north")
    if item then
        table.insert(result,item)
    end
    item = self:lookInDirection("south")
    if item then
        table.insert(result,item)
    end
    item = self:lookInDirection("east")
    if item then
        table.insert(result,item)
    end
    item = self:lookInDirection("west")
    if item then
        table.insert(result,item)
    end
    return result
end

I expect N and S to pass and E and W to get nil instead of OBSTACLE.

16: E: Look creates array of dictionaries  -- 
Actual: nil, 
Expected: OBSTACLE
17: W: Look creates array of dictionaries  -- 
Actual: nil, 
Expected: OBSTACLE

OK, there’s the Babe pointing over the fence. We fix the O problem:

    local names = { P="PIT", O="OBSTACLE" }

Tests should run. They do. Since this code is unused in the app, I can commit it. I feel OK with rough code that isn’t in production. Heck, on a given day, I feel OK with anything that works in production. I’m a human being, not a god. As far as you know.

Commit: new look function in process.

Sanding Out Some Roughness

Before I try to use this result, which I certainly could do, let’s improve this code a bit. It looks like this:

function World:look()
    local result = {}
    local item
    item = self:lookInDirection("north")
    if item then
        table.insert(result,item)
    end
    item = self:lookInDirection("south")
    if item then
        table.insert(result,item)
    end
    item = self:lookInDirection("east")
    if item then
        table.insert(result,item)
    end
    item = self:lookInDirection("west")
    if item then
        table.insert(result,item)
    end
    return result
end

function World:lookInDirection(direction)
    local directions = {
        east= {mx= 1,my= 0},
        west= {mx=-1,my= 0},
        north={mx= 0,my= 1},
        south={mx= 0,my=-1}
    }
    local names = { P="PIT", O="OBSTACLE" }
    local nsew= {east="E", west="W", north="N", south="S"}
    local dir = directions[direction]
    for xy = 1,5 do
        local fact = self:factAt(dir.mx*xy, dir.my*xy)
        if fact then
            return {
                direction=nsew[direction],
                type=names[fact],
                distance=xy
            }
        end
    end
    return nil
end

Let’s try doing that first blob with a loop:

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

Test. Green.

I’d really like just to have the look guy take care of the table for me. I think it’ll be cleaner.

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 directions = {
        east= {mx= 1,my= 0},
        west= {mx=-1,my= 0},
        north={mx= 0,my= 1},
        south={mx= 0,my=-1}
    }
    local names = { P="PIT", O="OBSTACLE" }
    local nsew= {east="E", west="W", north="N", south="S"}
    local dir = directions[direction]
    for xy = 1,5 do
        local fact = self:factAt(dir.mx*xy, dir.my*xy)
        if fact then
            table.insert(result, 
                {
                    direction=nsew[direction],
                    type=names[fact],
                    distance=xy
                }
            )
        end
    end
end

Top function is nice. Bottom not so much. Improve:

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)
    local names = { P="PIT", O="OBSTACLE" }
    for xy = 1,5 do
        local fact = self:factAt(dir.mx*xy, dir.my*xy)
        if fact then
            table.insert(result, 
            {
                direction=tag,
                type=names[fact],
                distance=xy
            }
            )
        end
    end
end

Tests green. Better. Improve:

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

OK, that’s nearly good. I might like to compare the scan approach to this one, because I know it’s a little different, and I did that amazing refactoring on it, but this is decent. Let’s check the names:

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

I don’t like the names much, mostly because of the passed accumulated result, which isn’t indicated in the method names. And who would like lookMxMy, especially since it isn’t passing Mx and My? I don’t have a better idea. I’ll let them rest a day and see what I think. The functions are broken up fairly well.

Commit, then break. Commit: Refactor new look code to be about a 0.8 quality.

I give it a B. Maybe a bit more than 0.8, maybe not. Anyway I need a break.

Break

I’ve been at it for two hours, and I am for some reaon a bit tired, so I’ll have a break. If I come back this afternoon, I might have this in for the demo. If not, that’s OK too, the demo does a bit more than we promised already.

I’d rather stop now than work tired, which only leads to miatskes and stress. I’ll push out this article with its main lesson, to me at least:

I’d prefer to push good code to the repo that’s not yet used, rather than push a “working” feature with poor code. I think over even the short term, I go faster, and I know for certain that I enjoy my work more. And let’s face it, they don’t pay us enough not to take pleasure in the work itself.

See you next time, later today, or tomorrow.



Yes, I know I misspelled that. Humor.