A bit of musing about the world, a bit of planning, a bit of coding. It’s Sunday, so this will probably be short.

Our Sunday ritual is a nice late breakfast and watching CBS Sunday Morning, which has enough news to keep me as up to date as I care to be, and generally some interesting and uplifting content as well. Since I’m the early riser in the family, I usually have a bit of time to program and/or write if the spirit moves me, which usually it does.

There are more important matters …

With the world as it is, programming and writing are escapes for me. The reality of programming is one where I’m pretty comfortable and, I hope, useful, and it keeps me from dwelling in an unhealthy way on the negative things around us.

I believe that the general trend of humanity is upward, but it’s clearly not a straight line. These days we have ridiculous wars, people trying to inject their personal mythology into the lives of others, endemic and systematic pushing down of some groups to the advantage of others—racism, sexism, you know the kinds of things I mean—destruction of the planet, and what seems to be an effective attempt to reduce or remove the ability of people to govern themselves.

Those are the important issues of our time, and we all need to work to improve things as well as we can. I find it a daunting set of problems, and honestly I can only bear to think about it so much. So programming and writing are safe havens for me.

You, too, need to find a safe haven, and you, too, need to find ways to work to improve the world, as you can. That’s important. This article is not, at least not starting here.

Walking Skeleton

I’ve used the term “walking skeleton” more in the past few articles than in the past many years, or so it seems. I fully agree with the notion, it’s just that I use other words. Either way, our mission here is to create a credible initial version of the product, which we can show to our stakeholders as evidence of progress and a foundation for feedback and planning—and then to keep that product growing and improving, always ready to ship if anyone wanted to have it.

My current vision of the initial “release” is to have a program with a world object in it, containing objects, responding to some commands, and serving up information, and a robot object in that world, with at least some ability to move and look around, shown on a rudimentary game-like screen display.

We’ve just completed code that places Obstacles or Pits in the world, where those objects are just represented by the letters O and P. We have a test, written previously, that connects the World and the Robot but that test is a stub:

        _:test("Robot updates knowledge on move", function()
            local world = World:test1()
            local robot = Robot(world)
            robot:scan()
            _:expect(robot:factAt(5,6)).is("fact")
            robot:move(1,1)
            _:expect(robot:factAt(4,5)).is("fact")
            _:expect(robot:factAt(5,6)).is(nil)    
        end)

function Robot:scan()
    self.knowledge = self.world:scan()
end

function World:scan()
    local knowledge = Knowledge()
    knowledge:addFact(Fact(5,6,"fact"))
    return knowledge
end

We see that the world just returns a single constant fact in response to scan.

I didn’t know we were going to do this a moment ago, but it seems to me that a useful thing to do would be to refactor the code supporting this test to make it more real. To that end, we might:

  1. Give World a member instance of Knowledge, in which it will keep track of its contents. (Already in place.)
  2. Change the test1 creation method to actually store the fact that is now returned as a constant.
  3. Change scan to return the relevant information.

There are issues with this, not least that when I wrote the test I wasn’t taking into account that a Robot can only see for a short distance, and only straight up and down and straight right and left. So we’ll modify the test first:

        _:test("Robot updates knowledge on move", function()
            local world = World:test1()
            local robot = Robot(world)
            robot:scan()
            _:expect(robot:factAt(3,0)).is("fact")
            robot:move(1,1)
            _:expect(robot:factAt(2,-1)).is("fact")
            _:expect(robot:factAt(3,0)).is(nil)    
        end)

I think that’s right. Now to fix scan to return the right literal:

function World:scan()
    local knowledge = Knowledge()
    knowledge:addFact(Fact(3,0,"fact"))
    return knowledge
end

And test. If anything fails, it’ll be the 2,-1 one.Unless I’m mistaken, which has been known to happen. Ha! Test runs.

Now let’s move the setup to test1:

function World:test1()
    local world = World()
    world:addFact(3,0,"fact")
    return world
end

And now we have to implement scan to use the “real” Knowledge. As our first step, with this test, we can just return our own, because it happens to be lined up properly:

function World:scan()
    return knowledge
end

I expect the test to run. I am a fool, of course, because I must say this:

function World:scan()
    return self._knowledge
end

NOW it should work. And it does.

Let’s now make the test harder, by placing one more thing into test1 world, that should not show up on the first scan.

function World:test1()
    local world = World()
    world:addFact(3,0,"fact")
    world:addFact(1,3,"invisible on first scan")
    return world
end

Now our test isn’t robust enough. Enhance it:

        _:test("Robot updates knowledge on move", function()
            local world = World:test1()
            local robot = Robot(world)
            robot:scan()
            _:expect(robot:factAt(3,0)).is("fact")
            _:expect(robot:factAt(1,3)).is(nil)
            robot:move(1,1)
            _:expect(robot:factAt(2,-1)).is("fact")
            _:expect(robot:factAt(3,0)).is(nil)    
        end)

This should fail, seeing the invisible object.

2: Robot updates knowledge on move  -- 
Actual: invisible on first scan, 
Expected: nil

Perhaps “not visible” is a better phrasing.

2: Robot updates knowledge on move  -- 
Actual: not visible on first scan, 
Expected: nil

Now we get to enhance scan. I think the rules are that you can see 5 cells in every direction, up down left and right. So:

function World:scan()
    local knowledge = Knowledge()
    for x = -5,5 do
        knowledge:addFact(self:factAt(x,0))
    end
    for y = -5,5 do
        knowledge:addFact(self:factAt(0,y))
    end
    return knowledge
end

I expect the test to pass. It does not:

2: Robot updates knowledge on move -- TestKnowledge:87: 
attempt to index a nil value (local 'aFact')

Ah, too fast. We need to pass the coordinates to our addFact:

function World:scan()
    local knowledge = Knowledge()
    for x = -5,5 do
        knowledge:addFact(x,0,self:factAt(x,0))
    end
    for y = -5,5 do
        knowledge:addFact(0,y,self:factAt(0,y))
    end
    return knowledge
end

And I expect trouble here because I don’t think we can do a table.insert of a nil. Test to find out.

2: Robot updates knowledge on move -- TestKnowledge:87: 
attempt to index a number value (local 'aFact')
function Knowledge:addFact(aFact)
    table.insert(self.facts, aFact:moveBy(self.x, self.y))
end

Well, sorry, I need to wrap the fact don’t I? This is irritating but it it telling us something about our methods: they don’t quite fit my mind. First make it work.

function World:scan()
    local knowledge = Knowledge()
    for x = -5,5 do
        knowledge:addFact(Fact(x,0,self:factAt(x,0)))
    end
    for y = -5,5 do
        knowledge:addFact(Fact(0,y,self:factAt(0,y)))
    end
    return knowledge
end

That passes. Let’s commit this: World:scan now returns only range 5 up down left and right.

We have nearly connected up robots and the world, but not quite. The world’s scan is supposed to be relative to the robot (and rotated, by the way, which we’ll come to sooner or later). As implemented, if we were to scan again, we should see the fact at 1,3 (and not the one at 3,0) but that won’t happen. Let’s extend the test:

        _:test("Robot updates knowledge on move", function()
            local world = World:test1()
            local robot = Robot(world)
            robot:scan()
            _:expect(robot:factAt(3,0)).is("fact")
            _:expect(robot:factAt(1,3)).is(nil)
            robot:move(1,1)
            _:expect(robot:factAt(2,-1)).is("fact")
            _:expect(robot:factAt(3,0)).is(nil)
            robot:scan()
            _:expect(robot:factAt(0,3)).is("not visible on first scan")
        end)

This should fail, because the scan doesn’t know we moved.

2: Robot updates knowledge on move  -- 
Actual: nil, 
Expected: not visible on first scan

Perfect. Making this work, however, will be a bit tricky, I think. Robots identify themselves to the world by their name. So our call to the world should include our name, like this:

function Robot:scan()
    self.knowledge = self.world:scan("Louie")
end

But the world doesn’t know who Louie is. Let’s just tell it:

        _:test("Robot updates knowledge on move", function()
            local world = World:test1()
            local robot = Robot("Louie",world)
            robot:scan()
...

And …

function Robot:init(name,aWorld)
    self.world = aWorld
    aWorld:launchRobot(name)
end

Test will fail looking for launch:

2: Robot updates knowledge on move -- TestRobot:39: 
attempt to call a nil value (method 'launchRobot')

But there’s another failure as well, because We have a test without a world:

        _:test("Robot Can be created", function()
            local robot = Robot()
        end)

I’m going to remove that test. It helped me at one point, but its days of usefulness are over. Remind me to talk about this other test though. But first:

1: Robot updates knowledge on move -- TestRobot:35: 
attempt to call a nil value (method 'launchRobot')
function World:launchRobot(name, robot)
    self._robots[name]=robot
end

This tells me that I need to pass in the robot. This is getting a bit wrong, but let’s hook it up first.

function Robot:init(name,aWorld)
    self.world = aWorld
    aWorld:launchRobot(name, self)
end

OK this should fail as before, with the non visible thing not showing up yet. No!!!

Wow, I am terrible at this prediction thing. I’m going too fast. Slow down, Ron. I forgot to init the _robots.

function World:init(width, height)
    self._width = self:validDimension(width or 1)
    self._height = self:validDimension(height or 1)
    self._knowledge = Knowledge()
    self._robots = {}
end

This really should give me the error I expect.

1: Robot updates knowledge on move  -- 
Actual: nil, 
Expected: not visible on first scan

Yes, perfect. Now to use the position of the robot in the scan. (And I think I see a bug in the test.)

Robots don’t know their position yet. Ahem minor oversight. When the connection is complete, they’ll get their position back from the call to launch. Let’s accept that as canon:

function Robot:init(name,aWorld)
    self._world = aWorld
    self._x, self._y = aWorld:launchRobot(name, self)
end

function World:launchRobot(name, robot)
    self._robots[name]=robot
    return 0,0
end

Now we do need to use those values and I’m going to rip them untimely from the robot:

function World:scan(robotName)
    local knowledge = Knowledge()
    local robot = self._robots[robotName]
    if not robot then return knowledge end
    local rx = robot._x
    local ry = robot._y
    for x = -5,5 do
        knowledge:addFact(Fact(x,ry,self:factAt(x,0)))
    end
    for y = -5,5 do
        knowledge:addFact(Fact(rx,y,self:factAt(0,y)))
    end
    return knowledge
end

I mentioned a bug. Our second fact was placed at 1,3, and our test checks 0,3. It should check 0,2:

        _:test("Robot updates knowledge on move", function()
            local world = World:test1()
            local robot = Robot("Louie",world)
            robot:scan()
            _:expect(robot:factAt(3,0)).is("fact")
            _:expect(robot:factAt(1,3)).is(nil)
            robot:move(1,1)
            _:expect(robot:factAt(2,-1)).is("fact")
            _:expect(robot:factAt(3,0)).is(nil)
            robot:scan()
            _:expect(robot:factAt(0,2)).is("not visible on first scan")
        end)

I expect this to work.It does not. Same error:

1: Robot updates knowledge on move  -- 
Actual: nil, 
Expected: not visible on first scan

Hm why not? Oh. I’ve not done the scan correctly. It has to adjust the coordinates. Eww, I’ve done worse than that. I’ll write it out longhand:

I still get this:

1: Robot updates knowledge on move after second scan -- 
Actual: nil, 
Expected: not visible on first scan

I should revert, but brekkers is just getting started, so I’ll debug a bit. Let me say what I’ve learned, though.

The whole Knowledge:addFact area is too messy. I keep having to strip out content and put it back. That can’t help. And the coordinate conversions are too explicit and need to be better packaged.

But I’d like to get this close to working, or at least understand what’s going on.

Oh my. I didn’t update the robot’s x and y when I moved him. :)

function Robot:move(x,y)
    self.knowledge:moveBy(x,y)
    self._x = self._x + x
    self._y = self._y + y
end

Massive debugging leaves me with this and the test running:

function World:scan(robotName)
    local knowledge = Knowledge()
    local fact, factContent
    local robot = self._robots[robotName]
    if not robot then return knowledge end
    local rx = robot._x
    local ry = robot._y
    --print("robot pos ", rx,ry)
    for x = -5,5 do
        --print("looking at x's", x+rx,ry)
        factContent = self:factAt(x+rx,ry)
        if factContent then
            print("x search found at ", x+rx,ry)
            fact = Fact(x+rx,ry,factContent):moveBy(-rx,-ry)
            print("returning ",fact)
            knowledge:addFact(fact)
        end
    end
    for y = -5,5 do
        --print("looking at y's", rx,y+ry)
        factContent = self:factAt(rx,y+ry)
        if factContent then
            print("y search found at ", rx, y+ry)
            fact = Fact(rx,y+ry,factContent):moveBy(-rx,-ry)
            print("returning ",fact)
            knowledge:addFact(fact)
        end
    end
    return knowledge
end

This is, of course, entirely unacceptable. Too many rx’s and ry’s, added and subtracted willy-nilly. I’ll remove the final check with a comment, and revert back to green.

Do I have a decent save point? I do not. OK, commit this. It works, it’s just horrid. Commit: World scan accommodates moving robot. HORRID CODE.

Let’s sum up what we’ve done and learned.

Summary

Overall, this is pretty patchwork stuff. We’re referring in the World to the actual robot, which in the final game will not be possible since it’s in a different computer. And our scan code, while it works, is far from obvious.

I think what I’d like to see is a sort of “lens” on the knowledge, that accepts an offset, presumably the robot’s coordinates, and returns facts using coordinates relative to that offset. That should be quite easy.

As for Facts, the method factAt doesn’t return a Fact, it returns fact contents, and addFact expects a fact, which we invariably seem to forget. So we need to improve that class.

All that said, look at what we have. We can now put obstacles and pits into a world, place a robot (without regard to the world contents as yet), and scan out information that is responsive to the robot’s motion.

That tells me that we can surely rig up a game display showing a robot on a grid with some stuff around it.

One more thing is on my mind, the need to have the world and robot communicating over a real or imaginary network. In particular, the game spec, which I am cruelly not letting you read because it doesn’t belong to me, specifies a JSON formatted string to be sent across the wires between robot and world. Before we’re done, we need to address that, which means that there won’t really be a direct call from Robot:scan to World:scan.

I propose to deal with that neatly, by injecting an object that understands Robot’s world-talking protocol on one side, and produces strings or tables on the other, and that takes in strings and tables and talks to the robot. And a similar object (or the same one) that reverses the process.

We’ll probably need to change Robot and World a bit from where they are now … but we are in the business of change, and we work to keep the changes as centralized as we can.

Will it go perfectly? Surely not. But I think it’ll go rather well.

For now, brekkers is nearly on the table, so I’ll hope to see you next time!