More on the JSON connection. I think I see something symmetric, if not actually easy.

What I’m about to say is obvious, I think, and yet I feel that there’s a way of looking at it that may be helpful: The twoo sides of the connection are entirely symmetric given this model. Since I’m building the Robot and World classes together, and since, so far, I’ve been calling World directly from Robot and returning results directly, the action of the communications part of the system is essentially to cancel itself out.

The Robot wants to say something to the world, so it says

world:something(blah, mumble)

The communications part is required to translate something, blah, and mumble into a valid command json string, as defined in the spec. On the other side, that string has to be decoded into exactly

world:something(blah, mumble)

Because that’s how we’ve written Robot and World: to communicate via method calls.

I’m not sure how that helps me, but it seems like it should, because what’s going on is something like this:

world:something(blah,mumble)
send json about something, blah, mumble
receive json about something, blah, mumble
world:something(blah,mumble)

OK. Therefore … what? Well, we are working on our WorldProxy. We’ve only just begun, but it can sit between robot and world and forward all the commands that we have so far. The point of that is to give us a place to stand, outside of both Robot and World, to deal with the communications aspect.

But for testing … can’t we connect our communications line to a test double, a fake world of some kind, that will tell us whether, after going through the json in-out, we make the right call? It seems possible.

My concern, which is still mostly vague in my mind, is that this translation from call to json to data to call has to be lossless. It has to be perfect. And it’s not clear just how to test it along the way, although we can of course test extensively in the game. Tests like that are slow and unreliable, as the tester (me) tends to forget to try certain things.

So I’m looking for ways to be confident that it’s going well.

Vague, unclear? Yes. So are my thoughts. That’s how things generally start out. Let’s see what we might try.

Where Are We? Where Should We Go?

We have a WorldProxy object, a trivial forwarder that has a World (or it could be anything that acts like a World) and it just forwards Robot messages and returns results. I am honestly not sure quite why I wrote that: I just wanted to break the direct connection between Robot and World, since when we are playing over a network, the connection is not direct.

Let’s see if we can do one command with a json conversion in it.

Curses. I’m still getting ahead of myself. The current World still has a direct connection to the real Robot. I really ought to sort that out first, but I also really want to try this idea, at least in a small way.

Let’s try scan, which is sort of nearly kind of maybe like what we want.

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

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

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

Without drilling down into all that, I’ll tell you that packets is an array of LookPacket instances, which are like this:

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

The spec for a scan result looks like this:

{
  result:"OK",
  data: {
    objects: [
      {
        direction: ...,
        type: ...
        distance: ...
      },
      ...
    ]
  }
}

I’ve left the quote marks off all the symbols. I think we get the idea. The LookPacket is a perfect match for the individual objects in the array, except that I used those underbar names. Silly me. It always seems to make sense until it doesn’t.

I really wanted to use the accessors in LookPacket, which is why I gave the members those names. But more important, I think, is the ability to go direct from LookPacket to json.

There is an alternative. We could require our objects to respond to asJSON, which would give them a chance to clean up if they needed to.

I’m not sure. Passing arbitrary things directly to json.encode seems risky, though it’s easy enough to test.

OK, when not sure, spike, and do things longhand rather than assuming answers to design questions. Let’s just make the World create a dictionary like the one above, and run in through json before returning it, and we’ll make the WorldProxy decode it for us. There will probably want to be other objects to help us but I don’t see them clearly yet.

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

Let’s change that to this:

    return self:jsonLookResult(packets)

And do this:

function World:jsonLookResult(packetArray)
    local outcome = {}
    outcome.result="OK"
    local objects = {}
    for i,p in ipairs(packetArray) do
        table.insert(objects,p)
    end
    outcome.data = { objects=objects }
    local json = json.encode(outcome, {indent=true})
    print(json)
    return packetArray
end

Note that I’m not returning the json yet. I want to look at it. This will keep the program running so that I can see what happened. Could I have written a test? Yes, but I’m spiking, experimenting, at this point. Let’s see what we get.

The tests cause this to produce a lot of output, and it looks good to me:

{
  "result":"OK",
  "data":{
    "objects":[{
        "_direction":"N",
        "_distance":3,
        "_type":"up"
      },{
        "_direction":"S",
        "_distance":2,
        "_type":"down"
      },{
        "_direction":"E",
        "_distance":5,
        "_type":"right"
      },{
        "_direction":"W",
        "_distance":4,
        "_type":"left"
      }]
  }
}

I’m not sure I’d have indented it that way myself, but it does look good except for those names with the underbars. Let’s try this:

function World:jsonLookResult(packetArray)
    local outcome = {}
    outcome.result="OK"
    local objects = {}
    for i,p in ipairs(packetArray) do
        table.insert(objects,p:asObject())
    end
    outcome.data = { objects=objects }
    local json = json.encode(outcome, {indent=true})
    print(json)
    return packetArray
end

We’ll ask the packets to return themselves as an object, meaning something suitable for the encoding. We’re just spiking here, to find a path we like. So:

function LookPacket:asObject()
    return {
        direction=self._direction,
        distance=self._distance,
        type=self._type
    }
end

And now they print like this:

{
  "result":"OK",
  "data":{
    "objects":[{
        "type":"up",
        "distance":3,
        "direction":"N"
      },{
        "type":"down",
        "distance":2,
        "direction":"S"
      },{
        "type":"right",
        "distance":5,
        "direction":"E"
      },{
        "type":"left",
        "distance":4,
        "direction":"W"
      }]
  }
}

OK, so that’s fine. Now if we return the json, everything in the universe will break. But the scan tests are all in Robot, so when we fix the proxy, everything should sort out.

I’ll return the json.

function World:jsonLookResult(packetArray)
    local outcome = {}
    outcome.result="OK"
    local objects = {}
    for i,p in ipairs(packetArray) do
        table.insert(objects,p:asObject())
    end
    outcome.data = { objects=objects }
    local jsonString = json.encode(outcome, {indent=true})
    return jsonString
end

I renamed the local to jsonString because it was confusing with the name json. Run the tests, watch the world burn.

Right. 11 tests fail. But we know why:

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

This guy gets a json string back now. Fix him up:

function WorldProxy:scan(...)
    local jsonString = self.world:scan(...)
    local outcome = json.decode(jsonString)
    local packets = outcome.data.objects
    local result = {}
    for i,p in ipairs(packets) do
        table.insert(result, LookPacket:fromObject(p))
    end
end

And …

function LookPacket:fromObject(o)
    return LookPacket(o.direction, o.distance, o.type)
end

I expect this to fix all the tests. Let’s find out how wrong I am.

I get a bunch of these:

1: Robot updates knowledge on move -- attempt to index a nil value

I do not know what I’ve done wrong. And I’m under time pressure, the breakfast cooking process has started.

Well, it’s a spike and I’m supposed to toss it anyway. Let’s try a few prints and see what happened.

The bug is, I forgot to return the result from the conversion. This is my standard error #1. Fix:

function WorldProxy:scan(...)
    local jsonString = self.world:scan(...)
    local outcome = json.decode(jsonString)
    local packets = outcome.data.objects
    print(#packets, " packets")
    local result = {}
    for i,p in ipairs(packets) do
        local lp = LookPacket:fromObject(p)
        print(lp)
        table.insert(result, lp)
    end
    return result
end

Test expecting good news.

Yes. Tests all run. Perfect. Now, what have we learned?

Spike Learning

Well, let’s see what this exercise has told us:

  1. Converting to and from json, and extracting what the robot call actually wanted is pretty straighforward, though a bit picky.
  2. It will help a lot if objects like LookPacket have names that match the json spec.

I’ve also learned that I don’t have a good feeling yet for these issues:

  1. What is a good arrangement of objects to handle the pipeline from call to robot-side json to world-side call to world-side return values to world-side json to robot-side return value?
  2. Is there an easy way to test all this conversion, not just to be accurate, but to be what we want?
  3. Would World be happier working more directly with objects that look like the input and output packets?

None of these troubles me much, but they do keep me aware that we are still working on a very flimsy walking skeleton. The ideas I’ve worked on today are a bit premature, I feel. It would be better to separate the World’s knowledge from the Robot’s, and to get the conversation to be a bit more like the spec.

In my defense, the spec only came out after I had started based on conversations with someone who knew someone who had met someone who had heard of the person who had the spec, so it’s no surprise that things don’t line up perfectly.

That said, the skeleton needs improvement, and that should probably take priority over more work with JSON, as interesting as that is. (No, really, it’s interesting, in that it is a very nice puzzle: what are good ways to pass Robot and World’s internal ideas back and forth through this arbitrary syntax.)

We’ll work on separation of Robot and World tomorrow. For today … brekkers and Sunday Morning.