We need to make the robot-world communication a bit more like the spec, and a bit more like client-server. Then we can make more progress.

I began this project before I really knew much about the official specification, and I made a decision that makes sense to me but that had characteristics that I didn’t foresee. I decided to write the program as if the Robot talked to the World in terms of methods and parameters and results that they “wanted”, that is, methods, parameters, and results that seem to fit nicely with the problems of being a robot or a world.

The unforeseen characteristics had to do with what the interface would turn out to be across a network, and what the eventually-arriving spec would say about the message formats. I wouldn’t so much say that I didn’t foresee those; it was more that I intentionally ignored them in the absence of information.

This is typical of my work here. I intentionally do not do as much planning, thinking, paper designing as one might, because I am a firm believer that we never get enough of that done in “real life”, and that when we do, it’s wrong anyway. So I model that aspect in my articles here, inevitably getting into trouble and then showing how it’s not so hard to get out, which is almost always the case.

Today I want to work toward a better separation of the World and Robot, and toward the messaging between them being a bit more like what we would need for a networked version. I see at least these steps:

  1. Change World to have its own representation of a robot instead of a pointer to the actual instance of Robot.
  2. Change operations one by one so that they go through the cycle of creating a request or reply object, converting to JSON, and converting back to what the program actually wants.

I suspect that #2 there will raise some design issues. In particular, since the World returns a somewhat standard reply package, amounting to a dictionary with known names, it’ll be tempting to write the code to deal directly with that package (and the corresponding request package). That temptation, if we succumb to it, will result in objects that know two things, how to deal with those packages, and how to be a world or a robot. Those concerns should, ideally, be separated into separate objects.

We’ll see. Let’s get started …

Robot Info

I’d like the World to have its own notion of the Robot, which may or may not include the same kind of information that our Robot class has. I think the answer is “may not”, because the Robot has a memory of what it has seen when it looked around, and the World will not keep that information on the individual robots, since it knows the whole world.

I think we’ll call the new thing RobotInfo … no, how about RobotStatus. Yeah, that.

I think the RobotStatus object may have no behavior. It might be pure data. We’ll find out quickly. But given that I don’t anticipate behavior (yet), I’m going to let the existing Robot and World tests drive out the RobotStatus object. We may find that we need RobotStatus tests. If so, we’ll change direction and write them.

It should all start with the launch. That’s here:

function Robot:setUpGame()
    local world = WorldProxy:setUpGame()
    return Robot("Louie", world)
end

function Robot:init(name,aWorld)
    assert(aWorld:is_a(WorldProxy), "expected WorldProxy")
    self._world = aWorld
    self._x, self._y = aWorld:launchRobot(name, self)
    self._name = name
    self.knowledge = Knowledge()    
end

Why isn’t knowledge underbarred? I don’t know. I’m not here for that, however, I’m here for this:

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

Let’s develop the habit of looking at the spec as we do this, now that we have a copy.

The robot launch protocol includes

robot=name,
command="launch"
arguments = {kind, shieldStrength, shots}}

The arguments are an array, not of named things but those values in order. Well, it is what it is. Let’s change our launch to expect those items:

function World:launchRobot(name, kind,shieldStrength, shots)
    local robot = RobotStatus(name, kind, shieldStrength, shots)
    self._robots[name]=robot
    return robot:launchStatus()
end

There’s a big assumption in here, the robot:launchStatus. And there goes my theory that there’s no behavior on the RobotStatus. I think this is too much for one bite. We’ll settle for returning x, y and direction. (The current robot side doesn’t expect direction, but that’ll be fine, I think.)

So let’s pare it down to this:

function World:launchRobot(name, kind,shieldStrength, shots)
    local robot = RobotStatus(name, kind, shieldStrength, shots)
    self._robots[name]=robot
    return robot._x, robot._y, robot._direction
end

One more bit of speculation: to initialize the RobotStatus, we’ll need info from the World, so let’s pass it in to the constructor:

function World:launchRobot(name, kind,shieldStrength, shots)
    local robot = RobotStatus(self, name, kind, shieldStrength, shots)
    self._robots[name]=robot
    return robot._x, robot._y, robot._direction
end

Since there is only one world for this robot, I think it’s OK for the world to be known. We can test this to see how it blows up, which will be on RobotStatus.

5: Robot moves forward on 1 key -- TestWorld:117: attempt to call a nil value (global 'RobotStatus')

We get a plethora of messages like that one. Build the class:

function RobotStatus:init(world, name, kind, strength, shots)
    self._world = world
    self._name = name
    self._kind = kind
    self._strength = strength
    self._shots = shots
    self._x, self._y, self._direction = World:placeRobot()
end

I’ve posited a new method, placeRobot. For now that can be trivial:

function World:placeRobot()
    return 0,0,"N"
end

Test. I expect we’re built but I’m not sure what’s going to happen with robot operations.

1: Robot updates knowledge on move after second scan -- 
Actual: nil, 
Expected: not visible on first scan
5: Robot moves forward on 1 key  -- 
Actual: nil, 
Expected: O

I think we’re going to need to deal with move, because it doesn’t even pretend to ask the world:

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

OK, here again we’re supposed to send in a request package and we’re not really doing that. Nor have we really done that with the launch. Let’s make it work, then make it right. So … first I make this method a bit more clear:

function Robot:move(dx,dy)
    self.knowledge = self.knowledge:newLens(dx,dy)
    self._x = self._x + dx
    self._y = self._y + dy
end

Now another fact that I happen to know is that we can only move forward or back. I hate working on a red bar, though. Make it work, then make it right.

function Robot:move(dx,dy)
    local x,y = self._world:move(dx,dy)
    newDx = x - self._x
    newDy = y - self._y
    self.knowledge = self.knowledge:newLens(newDx,newDy)
    self._x = x
    self._y = y
end

This will drive out move in World:

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

And, writing the method tells me I missed a parameter on the call:

function World:move(robotName,dx,dy)
    local robot = self._robots[robotName]
    robot._x = robot._x + dx
    robot._y = robot._y + dy
    return robot._x, robot._y
end

So fix that:

function Robot:move(dx,dy)
    local x,y = self._world:move(self._name, dx,dy)
    newDx = x - self._x
    newDy = y - self._y
    self.knowledge = self.knowledge:newLens(newDx,newDy)
    self._x = x
    self._y = y
end

Test. Forgot to put the method on proxy:

function WorldProxy:move(...)
    return self.world:move(...)
end

Test. Tests are green. Commit: Initial RobotStatus in World.

This is good news, and almost surprising. We have disconnected the World from the Robot: it only knows its own version of the information, and we’ve begun passing it back and forth.

I’ve not tested what happens in the display. If it doesn’t work, I’m short of some tests. But it does. Even better news.

Move: Try #1

I think it’s time to begin sending more request packets from Robot to world. At present the design is that that is supposed to take place in the WorldProxy, which translates to and from JSON and such … although it has only just begun to do that, and we’ll surely want more objects then just this one.

Let’s do move. The move packet is supposed to look like this:

{
    robot=name,
    command: "forward" or "back",
    arguments={ steps }
{

And we know that the “real” operations on the robot are to move forward or backward some steps, so let’s prepare for that at least a bit. My first cut is this:

function WorldProxy:move(robot,y) -- assume forward
    local request = {
        robot=robot._name,
        command="forward",
        arguments={y}
    }
    local jsonRq = json.encode(request)
    local jsonOut = self.world:request(jsonRq)
end

I’ve changed the calling sequence, too. I need the robot name, there’s no way out of that. But I don’t need to do all that other stuff. Let’s take a Much Smaller Step.

function WorldProxy:move(name, y) -- assume forward
    return self.world:move(0,y)
end

And I do have to fix the one caller:

function Robot:move(dx,dy)
    local x,y = self._world:move(self._name, dy)
    newDx = x - self._x
    newDy = y - self._y
    self.knowledge = self.knowledge:newLens(newDx,newDy)
    self._x = x
    self._y = y
end

I expect this to continue to work. Doesn’t. I’m not sure what I did. Let’s just revert and do again.

Move: Try #2

I think I fumbled one of the balls that I had in the air. But I learned some things not to do.

Here’s where we stand now:

function Robot:move(dx,dy)
    local x,y = self._world:move(self._name, dx,dy)
    newDx = x - self._x
    newDy = y - self._y
    self.knowledge = self.knowledge:newLens(newDx,newDy)
    self._x = x
    self._y = y
end

function WorldProxy:move(...)
    return self.world:move(...)
end

function World:move(robotName,dx,dy)
    local robot = self._robots[robotName]
    robot._x = robot._x + dx
    robot._y = robot._y + dy
    return robot._x, robot._y
end

Let’s admit, on the Robot side, that this method is forward and implement this:

function Robot:keyboard(key)
    if key == "s" then
        self:scan()
    elseif key == "1" then
        self:forward(1)
    elseif key == "r" then
        local lens = RotatingLens(self.knowledge)
        lens:turn("right")
        self.knowledge = lens
    end
end

function Robot:forward(steps)
    local x,y = self._world:forward(self._name, steps)
    newDx = x - self._x
    newDy = y - self._y
    self.knowledge = self.knowledge:newLens(newDx,newDy)
    self._x = x
    self._y = y
end

This will fail for want of forward on the proxy:

5: Robot moves forward on 1 key -- TestRobot:135: attempt to call a nil value (method 'forward')

So:

function WorldProxy:forward(name,steps)
    return self:move(name,0,steps)
end

I’ve not removed any of the move methods yet. I expect this to work. It does. Commit: “1” command does forward not move. (WorldProxy still does move).

Much Smaller Step works. Now let’s get more like a request in WorldProxy.

OK, I just went nuts here:

function WorldProxy:forward(name,steps)
    local result
    local rq = {
        robot=name,
        command="forward",
        arguments={steps}
    }
    local jsonRq = json.encode(rq)
    local decoded = json.decode(jsonRq)
    local dName = decoded.robot
    local dCommand = decoded.command
    if dCommand == "forward" then
        local dSteps = decoded.arguments[1]
        return self:move(dName,0,dSteps)
    end
    assert(false,"impossible command "..dCommand)
end

All in line, I created the request packet, encoded it, decoded it, unravelled it, and used it in self:move, which looks like this:

function WorldProxy:move(...)
    return self.world:move(...)
end

But instead let’s call forward on World:

    if dCommand == "forward" then
        local dSteps = decoded.arguments[1]
        return self.world:forward(dName,0,dSteps)
    end

And fail because that doesn’t exist yet.

5: Robot moves forward on 1 key -- TestWorldProxy:57: attempt to call a nil value (method 'forward')

And build it:

function World:forward(robotName,steps)
    local robot = self._robots[robotName]
    robot._y = robot._y + steps
    return robot._x, robot._y
end

Expect green. Be disappointed:

5: Robot moves forward on 1 key  -- 
Actual: 0, 
Expected: 1
5: Robot moves forward on 1 key  -- 
Actual: nil, 
Expected: O

Check that test:

        _:test("Robot moves forward on 1 key", function()
            local world = WorldProxy()
            world:addFactAt("O",3,1)
            local robot = Robot("Louie", world)
            _:expect(robot._y).is(0)
            robot:scan()
            _:expect(robot:factAt(3,0)).is(nil)
            robot:keyboard("1")
            _:expect(robot._y).is(1)
            robot:scan()
            _:expect(robot:factAt(3,0)).is("O")
        end)

OK, there it is: robot’s y came back zero. Must be a bug. Let’s see if we can find it quickly.

World:forward looks OK:

function World:forward(robotName,steps)
    local robot = self._robots[robotName]
    robot._y = robot._y + steps
    return robot._x, robot._y
end

It returns both x and y.

WorldProxy is wrong: I forgot to remove the 0 for x:

        return self.world:forward(dName,0,dSteps)

Fix:

        return self.world:forward(dName,dSteps)

Green. Commit: “1” command now uses forward from end to end. JSONization all in Proxy.

We should be able to remove all the move code, but should look to be sure there are no calls, and, if there are, change them to forward.

I thought I did that right but:

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

Revert? First I’ll look. Not obvious. Revert. Much Smaller Step. What does move do that forward does not? In Robot, they’re the same as far as I can see:

function Robot:forward(steps)
    local x,y = self._world:forward(self._name, steps)
    newDx = x - self._x
    newDy = y - self._y
    self.knowledge = self.knowledge:newLens(newDx,newDy)
    self._x = x
    self._y = y
end

function Robot:move(dx,dy)
    local x,y = self._world:move(self._name, dx,dy)
    newDx = x - self._x
    newDy = y - self._y
    self.knowledge = self.knowledge:newLens(newDx,newDy)
    self._x = x
    self._y = y
end

In the Proxy? Very different, but netting it all out they just call world forward or world move:

function World:forward(robotName,steps)
    local robot = self._robots[robotName]
    robot._y = robot._y + steps
    return robot._x, robot._y
end

function World:move(robotName,dx,dy)
    local robot = self._robots[robotName]
    robot._x = robot._x + dx
    robot._y = robot._y + dy
    return robot._x, robot._y
end

I don’t see it. OK, change one of the tests to call forward and figure it out the hard way.

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

Test. And it fails:

1: Robot updates knowledge on move  -- 
Actual: nil, 
Expected: fact

Check to see if we moved as we expect.

Oh hell. The move was (1,1). That’s no longer possible. I carefully converted (1,1) to (1), twice. Got to recast this test.

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

And I had to change the setup as well:

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

I was supposing that I could move diagonally, which is not the case. Anyway, back to green. Let’s go about removing the move methods again.

Green this time. Commit: Remove unused move method throughout.

OK, it’s time to reflect and explain my “thinking”.

What Are You Thinking???

The new forward method in the proxy is wild:

function WorldProxy:forward(name,steps)
    local result
    local rq = {
        robot=name,
        command="forward",
        arguments={steps}
    }
    local jsonRq = json.encode(rq)
    local decoded = json.decode(jsonRq)
    local dName = decoded.robot
    local dCommand = decoded.command
    if dCommand == "forward" then
        local dSteps = decoded.arguments[1]
        return self.world:forward(dName,dSteps)
    end
    assert(false,"impossible command "..dCommand)
end

What’s that all about?

Well, I wanted to take a first cut at showing all the steps that go into making a request to the World server and getting something back. This is just the request side:

  1. Create a command request as a table (or object);
  2. Convert it to JSON
  3. (Send it to the server)
  4. Convert it back to an object
  5. Based on what’s in the object, pull out the necessary information and do the work.

Not shown (yet) is that the return from the server is another line of JSON, representing a much more complicated table, which we have to unravel to reflect the information in our Robot.

My reason for doing all that was that, since I was doing a new and more legitimate command, “forward”, I wanted to see the sequence clearly, all written out. My vague vision for how this will all work is that the Robot will think in terms of what it wants: world:forward(n). Some object, probably standing where WorldProxy stands now, will create a request and pass it to another object that will convert it to a JSON string and send it to the server (or possibly pass it to another (client) object that sends it … it’s too soon to be sure). The server will receive the JSON and convert it back to a request, which it will give to another object probably a RobotProxy, that will make the call to the World. The world returns, the proxy packs up a response, returns it to the server, which converts it to JSON, sends it back to the client object who decodes it and returns it to the proxy, which informs the robot of its new status.

You’re probably thinking that that paragraph needs to be broken up, and that is a signal that the method needs to be broken up. That’s fine: I just wanted to get it all down so that I could start seeing the flow and start breaking it up.

Issue

Having done so I see at least one somewhat large issue. The current Robot doesn’t do much but its focus is on asking a specific question and getting a specific answer. The client-server design is more “make a request that could be anything, get back a really big answer that has what you wanted to know in it somewhere”. I think that in the end, that’ll be fine, we’ll just pack away all the information and then, if we need to, check our own status to see if we need to display something differently, make an exploding sound, or whatever we might do.

In the interim, however, it’s going to be a bit messy.

Would it have been better …

Would it have been better if we had started right off with information packets going back and forth between Robot and World? I guess it depends what we mean by “better”.

I think that passing big dumb structures back and forth between objects is a crock. Knowledge is lost and has to be recovered, going and coming. We see that in the big forward method above, where we’re already packing u perfectly meaningful numbers like step into meaningless arrays like arguments and then needing to pull them out by a supposed index and reinflate them to use them. That’s nasty and error-prone, so we’d rather not.

In the client-server mode, we have no choice but to boil down and reconstitute, but we don’t have to let that activity contaminate all our thinking.

As we go forward, we’ll try to keep the boiling and reconstituting isolated. Right now, it’s not isolated very well, but as we begin to see more of it, we’ll start moving things together.

Meanwhile, we’re now speaking to the World through a Proxy that has the responsibility to do the translation we need. We’re getting there, incrementally … as is our fashion.

For today, we’ve got the problem mostly surrounded in the forward method, though a bit of it has turned up in launch. Over the next few days, we’ll make it better and make it do more things. I think being able to turn would be nice, for example.

ERRATUM

Astris Sawatzsky asked why the test below referred to (1,3):

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

She was right to do so. There is an object at (3,1) that should not be seen on the first scan, and the test was trying to check for that. The correct test is:

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

Commit: Fix test per Astrid. Thanks, Astrid!

However …

Learning (Again)

At least a few times today, I started to take a bite that would require me to change a large number of lines all over the world. I think I stopped myself in time once. Twice I had to revert. Once I did write large method.

The stopping in time was good: I saw that I needed a Much Smaller Step. The reverts were my reaction to having my nose rubbed in the fact that I needed Much Smaller Steps. And the big method, well, I know how to have done that in smaller steps. I didn’t, and that time I got away with it.

I am fascinated. I’m a pretty good programmer, and I can generally make a change that requires me to change three, maybe four places, before it all works again. I usually make it work. And yet, sometimes I don’t, and it turns out there is always a way to do it in much smaller steps I might have to change two places, a call and a method. Often I can do it inside the method, in one place, and then pull it out.

I’m not as good at Many More Much Smaller Steps as I could be. Fascinating to be as wonderful as I am and still have things to learn. (Some sarcasm there, forgive me. But I sure do have things to learn.) I’ll keep trying.

See you next time!