Robot 20
The spec requires us to communicate using JSON. It’s time to get started with that. As I type this I wonder something …
The spec for the official student version of Robot Worlds specifies JSON-format data representing commands and command replies. They’re pretty standard-looking things, about what you’d imagine, named constants and named arrays of named constants. Lots of details, nothing surprising.
It turns out that Codea includes json.decode
and json.encode
. I’ve tested those a bit, using Lua tables, on my other iPad, and they’ll do the job. But I just got an idea.
Suppose the format for the message requesting a left turn looked like this:
{
"robot": "Louie",
"command": "turn",
"args": [ "left" ]
}
You should suppose that because it’s true. Now if we define this table in Codea Lua:
local tab = {
robot="Louie",
command="turn",
args={ "left" }
}
It turns out that this table will convert to the JSON we desire. No surprise so far, but here’s what I realized today:
Every object in Lua is a table. So we could, I believe, create a command object with useful methods, and pass the object to json.encode
and we should get the right json. If that works, it would be convenient. If not, well, we can still have the object and have it contain a dictionary. But having it be the thing encoded might be better.
I need some tests. These will be tricky, because we’re not guaranteed any particular order from the json.encode
. Anyway let’s see what we can see.
I wrote a rather optimistic test:
Command JSON Testing
_:test("Command", function()
local cmd = Command("turn"):arg("left")
local encoded = json.encode(cmd)
print(encoded)
local decoded = json.decode(json)
_:epect(decoded.robot).is("Louie")
end)
I think I’d like a “fluent interface” to create a command, where we just string together method calls until we have what we want. I don’t expect that to be terribly useful for commands, which are pretty simple, but for returning results I think it’ll be more useful. Possible YAGNI, but this is my spec and the fluent interface is part of what I want to test.
Too much in one go? Maybe. We’ll see. Anyway, after the command is encoded, I print it, because I want to see it, and because it’ll be hard to test as a string, I decode it and prepare to test the resulting dictionary.
When I run this test I expect to drive out the Command class:
1: Command -- TestCommand:16: attempt to call a nil value (global 'Command')
OK:
Command = class()
This will drive out arg. And I realize that I need to expand my test, which I’ll do before I forget:
_:test("Command", function()
local cmd = Command("turn"):arg("left")
local encoded = json.encode(cmd)
print(encoded)
local decoded = json.decode(json)
_:expect(decoded.robot).is("Louie")
_:expect(decoded.command).is("turn")
_:expect(decoded.args[1]).is("left")
end)
I also corrected the spelling error in the first test. Test now, should ask for arg:
1: Command -- TestCommand:16: attempt to call a nil value (method 'arg')
Shall I play it cool and just implement a null method, letting the TDD drive all the way? No. We know what we need, and while it is really fine to let TDD do everything, it’s not a law of man or nature. I’m going to code a bit of sense into this thing:
function Command:init(command)
self.robot = "Louie"
self.command = command
self.args = {}
end
function Command:arg(anArg)
table.insert(self.args, anArg)
return self
end
If this works, my optimism wasn’t excessive. If it doesn’t, we know that Ron should have used Many More Much Smaller Steps.
Well. First it prints this:
{"args":["left"],"robot":"Louie","command":"turn"}
That looks quite decent, but then it prints this:
1: Command -- ...EAF/Codea.app/Frameworks/RuntimeKit.framework/dkjson.lua:693: bad argument #2 to 'pegmatch' (string expected, got table)
LOL. That’s because I said this:
local decoded = json.decode(json)
Instead of this:
local decoded = json.decode(encoded)
I refuse to embarrass myself by explaining why I typed the first thing. Anyway test again hoping for a good result.
The test passes. One more thing: the Codea json stuff can format. Let’s do that:
local encoded = json.encode(cmd, {indent=true})
Test again to look at the print now.
{
"command":"turn",
"args":["left"],
"robot":"Louie"
}
Nice. And, as we see, things are in arbitrary order. That’s going to make testing a bit tricky.
However, most importantly, this tells me that I can create a command object, and whatever member variables it has, within reason, will be encoded. And, in turn, this means that I can have a command object that can be tested.
I think the Command protocol isn’t good, though. I change the test to my best guess at what we’ll want:
_:test("Command", function()
local fakeRobot = { name="Louie" }
local cmd = Command(fakeRobot):command("turn"):arg("left")
local encoded = json.encode(cmd, {indent=true})
print(encoded)
local decoded = json.decode(encoded)
_:expect(decoded.robot).is("Louie")
_:expect(decoded.command).is("turn")
_:expect(decoded.args[1]).is("left")
end)
I think we want the command created with the robot, and to have it fetch the name. I’ll allow the fetch to the member variable, because we’re all friends here. Then you do a command, and then args, fluently.
The code in support of this is:
function Command:init(robot)
self.robot = robot.name
self.args = {}
end
function Command:command(aCommand)
self.command=aCommand
return self
end
function Command:arg(anArg)
table.insert(self.args, anArg)
return self
end
I think we’ve learned what we set out to learn, namely that we can encode an object into JSON so long as it has member variables whose names are those of the JSON we want. And we have a nice start at a fluent interface for creating commands (and, later, results coming back).
Now we have a larger problem.
Robot and World Communicate vis JSON
In the actual spec, the pattern is that the Robot side formats a JSON command and passes it over a socket to the World side, which decodes it, updates itself as needed, and then formats a JSON result and sends it back to the Robot side, over the same socket.
Our design isn’t like that. In our design, our robot has a world member variable and she calls methods on it, methods that (mostly) make sense to us as developers, and she receives results back as a return from the method call.
We can respond to this situation in at least two ways.
We can be like “Oh no we’ve screwed up the design” and go into a panic revising our Robot and World, trying to salvage whatever we can.
Or we can be like “Yes, we believe that our objects should have calling sequences pertinent to them and that we should deal with external formats like this bloody JSON separately”.
We’re going to be like the second one. In fact, I knew—and you knew—that this day was coming. I wanted it to come, because I wanted to figure out how to adapt my desired calling sequences to the required messaging format.
Yeah, right. Anyway we’re here now, what are we going to do?
A Proxy / Adapter
There’s a very nice writeup and picture of the Adapter pattern on refactoring.guru. It’s a very simple notion: we “just” convert what we have into the format that they want. And, in our case, we’ll have to convert the thing they return back into what we want.
We’ll deal with the calling side first, because it’s simpler and therefore probably easier.
Our cunning plan is to replace the World instance that the Robot has with a proxy object. Proxy is also described on refactoring.guru. The World proxy will be responsible for being, or having, the necessary adapters to do the conversion to and from JSON. Whether this proxy will be or contain the adapters, we don’t know. We’ll evolve the code, as we always do, and keep it clear and clean, and see where we wind up.
I am honestly not sure just how this is going to go. My ability to envision things accurately is not that powerful. But I am sure that I can write some tests that will do the job of fleshing out this idea.
Let’s try that.
World Proxy / JSON
We need some tests for this. New tab.
-- TestWorldProxy
-- RJ 20220608
function test_WorldProxy()
_:describe("WorldProxy", function()
CodeaUnit_Detailed = false
_:before(function()
end)
_:after(function()
end)
_:test("WorldProxy", function()
_:expect(3).is(2)
end)
end)
end
Test should hook up and fail.
1: WorldProxy --
Actual: 3,
Expected: 2
Oh, I forgot a commit. Let’s commit that Command test.
What would be a good first test? We really only have a few Robot commands at this point, scan, move, and turn(right).
I was going to start with scan, just to show you how tough I am, but I’ve decided, no, let’s start with move because it’s trivial and it’s actually going to drive out a lot of behavior if we let it. The reason is that none of our commands are really fully implemented, dealing with all the information that comes back from the robot. And this will be a great chance to figure out what to do with all that.
Let me say right now that I think this may get a bit messy for a while. Our walking skeleton is just a skeleton, and we’re going to have to put meat on its bones in a number of places. (My apologies for that image.)
But we won’t let it stay messy, will we? No, we won’t.
Here’s move now:
function Robot:keyboard(key)
if key == "s" then
self:scan()
elseif key == "1" then
self:move(0,1)
elseif key == "r" then
local lens = RotatingLens(self.knowledge)
lens:turn("right")
self.knowledge = lens
end
end
function Robot:move(x,y)
self.knowledge = self.knowledge:newLens(x,y)
self._x = self._x + x
self._y = self._y + y
end
Now this is flat wrong. We can only move “forward” or “back”. And we can move more than one step. (I’m not sure what the limit is. And our robot isn’t properly handling his turns yet. See what I mean by messy? We have dangling threads all over. Let’s just keep going. How does the World currently know where we are?
We’ll have to check scan, I think.Oh wow:
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
The World, no surprise, has a robot instance that it is interrogating for x and y … and it is the “real” robot, the same one we created. I think we’re not quite ready for our WorldProxy / JSON story. It’s too big a step. Let’s back off and brainstorm some tasks.
Tasks for WorldProxy/JSON
In no particular order:
- World should have its own robot or proxy
- Robot should have a WorldProxy. Trivial?
- World should place robot
- Robot should get position from World
- Robot should tell world “forward” or “back”, then receive new position
- Robot should tell world “turn”, then receive new position and direction.
I think I’d like to begin by creating a WorldProxy and using it. I expect it’ll break everything, but there isn’t much everything to break.
Back to the tests:
Grr. After some messing about with the above tasks, I’ve decided that I have to do something even simpler than I had imagined. Many More Much Smaller Steps.
Revert to a clean point.
Simple WorldProxy
I think my first task is to build a WorldProxy object that does nothing but correctly forward my current world messages to a contained World. And I want to use that world proxy everywhere, that is, never again to allow the Robot to talk directly to a world.
That’s a bit tricky because I have some intricate setup in my tests already.
I think the rule will be that creating a WorldProxy creates a World instance. Of course the final real one will connect to a world.
_:test("WorldProxy", function()
local proxy = WorldProxy()
_:expect(proxy).isnt(nil)
end)
Just to get going. I think the rule should be that a WorldProxy comes back connected to “the” world.
1: WorldProxy -- TestWorldProxy:16: attempt to call a nil value (global 'WorldProxy')
WorldProxy = class()
function WorldProxy:init()
end
Check for a world:
_:test("WorldProxy", function()
local proxy = WorldProxy()
_:expect(proxy).isnt(nil)
_:expect(proxy.world).isnt(nil)
end)
I could check more deeply but that’ll do.
Worlds want to be created with a width and height. Hmm. The default is 1,1. We’ll go with that …
function WorldProxy:init()
self.world = World()
end
Test runs. Commit: initial WorldProxy.
Now let’s see who calls World and see what we have to do to make them use the proxy. There are a lot of tests for World. Those are fine.
The Robot tests create a lot of worlds. Let’s change those one at a time and make them work:
_:test("Scan on all four sides", function()
local world = World()
local robot = Robot("Louie",world)
world:addFactAt("right",5,0)
world:addFactAt("left", -4,0)
world:addFactAt("up", 0,3)
world:addFactAt("down",0,-2)
robot:scan()
_:expect(robot:factAt(5,0)).is("right")
_:expect(robot:factAt(-4,0)).is("left")
_:expect(robot:factAt(0,3)).is("up")
_:expect(robot:factAt(0,-2)).is("down")
end)
Change that to WorldProxy and see what it says.
2: Scan on all four sides -- TestRobot:102: attempt to call a nil value (method 'launchRobot')
OK, that’s in creating the Robot:
function Robot:init(name,aWorld)
self._world = aWorld
self._x, self._y = aWorld:launchRobot(name, self)
self._name = name
self.knowledge = Knowledge()
end
Now we want our proxy, in the fullness of time, to be able to act exactly like a world, so our mission is just to implement everything it needs, forwarding to the world:
function WorldProxy:launchRobot(name, robot)
return self.world:launchRobot(name,robot)
end
We’ll of course be changing all these calls to use JSON at some point in the hopefully near future. First we gotta get it hooked up.
Test. I reckon it’ll be about adding facts.
2: Scan on all four sides -- TestRobot:31: attempt to call a nil value (method 'addFactAt')
And …
function WorldProxy:addFactAt(fact,x,y)
return self.world:addFactAt(fact,x,y)
end
Test.
2: Scan on all four sides -- TestRobot:141: attempt to call a nil value (method 'scan')
Sure, OK:
function WorldProxy:scan()
return self.world.scan()
end
Test.
2: Scan on all four sides -- TestWorld:122: attempt to index a nil value (local 'self')
Huh. Interesting. Oh. Typed . not :
When I fix that, I get a raft of errors:
2: Scan on all four sides --
Actual: nil,
Expected: right
2: Scan on all four sides --
Actual: nil,
Expected: left
And so on. Here’s the test. It’ll be the factAt calls failing:
_:test("Scan on all four sides", function()
local world = WorldProxy()
local robot = Robot("Louie",world)
world:addFactAt("right",5,0)
world:addFactAt("left", -4,0)
world:addFactAt("up", 0,3)
world:addFactAt("down",0,-2)
robot:scan()
_:expect(robot:factAt(5,0)).is("right")
_:expect(robot:factAt(-4,0)).is("left")
_:expect(robot:factAt(0,3)).is("up")
_:expect(robot:factAt(0,-2)).is("down")
end)
I rather expected this to go perfectly. Ah:
function Robot:scan()
local packets = self._world:scan(self._name)
for i,packet in ipairs(packets) do
self:addLook(packet)
end
end
All my proxy calls have possible arguments and should just pass them on. Rookie mistake.
function WorldProxy:addFactAt(...)
return self.world:addFactAt(...)
end
function WorldProxy:launchRobot(...)
return self.world:launchRobot(...)
end
function WorldProxy:scan(...)
return self.world:scan(...)
end
Tests go green. Commit: WorldProxy passes one robot test.
Back to searching for “World(“ to find people creating worlds directly.
This one is tricky … It does pass …
_: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)
But we know that it shouldn’t pass, not really, because it’s moving the local robot and the local robot is being used in World to get the position. I don’t want to put in a red test. We’ll let it be but make a “card”: robot move is not round trip. (Yeah, no, I’m not going to put it in Jira.)
Moving on … the next few occurrence in Robot aren’t teaching us anything, they were just there to allow the creation of a robot. Which does mean they’re calling launch, but we don’t mind that.
At this point, there is one creation or World in WorldProxy, and some in TestWorld, and no others. But there is another case, people sending messages to World class. Search for “World:”
_: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), "after second scan").is("not visible on first scan")
end)
Change that to WorldProxy and fix the forwarding.
function WorldProxy:test1(...)
return World:test1(...)
end
Test. All green and the game runs. I am not convinced. Let’s check all our Robot creations … in fact, let’s go hard:
function Robot:init(name,aWorld)
assert(aWorld:is_a(WorldProxy), "expected WorldProxy")
That should blow the doors off. Hm not too bad:
1: Robot updates knowledge on move -- TestRobot:101: expected WorldProxy
3: Set Up Game -- TestRobot:101: expected WorldProxy
Oh bad Ron: the test1 can’t do this:
function WorldProxy:test1(...)
return World:test1(...)
end
It’s a constructor and must do this:
function WorldProxy:test1(...)
local world = World:test1(...)
local proxy = WorldProxy()
proxy._world = world
end
That’s nasty. Why? Because I have to patch in a world into one that I create? Why? Because creation of a WorldProxy creates a world and we haven’t sorted that out yet:
function WorldProxy:init()
self.world = World()
end
Now I briefly considered passing the world in to my init
but I think the usual world creation will be to specify some parameters, so I was reluctant to make that not work. No, I think it’s better than just leaving this nasty in, which we might not notice. Change:
function WorldProxy:test1(...)
local testWorld = World:test1(...)
local proxy = WorldProxy(testWorld)
end
function WorldProxy:init(aWorld)
self.world = aWorld or World()
end
Test expecting green. Denied:
1: Robot updates knowledge on move -- TestRobot:101: attempt to index a nil value (local 'aWorld')
3: Set Up Game -- TestRobot:101: expected WorldProxy
It would be nice if I returned the proxy from the creation method, wouldn’t it?
function WorldProxy:test1(...)
local testWorld = World:test1(...)
return WorldProxy(testWorld)
end
That should fix the one.
3: Set Up Game -- TestRobot:101: expected WorldProxy
Find that:
_: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 Robot:setUpGame()
local world = World:setUpGame()
return Robot("Louie", world)
end
We can make this refer to WorldProxy. I think that’ll do to drive out the method on WorldProxy. Test:
3: Set Up Game -- TestRobot:96: attempt to call a nil value (method 'setUpGame')
Right.
function WorldProxy:setUpGame()
local gameWorld = World:setUpGame()
return WorldProxy(gameWorld)
end
Test expecting green. Green is provided. Game walking skeleton demo still walks. Commit: MakingApp runs on WorldProxy throughout, simple forwarding only.
It’s 1220, I started at 0930, so it’s time to sum up.
Summary
Bit of a rocky start. I tried to juggle too many ideas, a proxy, and JSON and forwarding and results, so I was kind of thrashing. Not a pretty sight.
So I backed away slowly, holding my hands up the way that you do when raptors are looking at you as if you might be lunch. I reverted, and did something simpler: I put in a WorldProxy that just forwards method calls directly to a World instance. That was straightforward and easily guided by tests. Because I stuffed that assert
in there, I am doubly certain that no one will ever pass a plain World to the Robot ever again.
Along the way I was reminded that the walking skeleton is pretty slim, and that we aren’t using the world-robot connection the way we should, where the robot asks to do things and is informed by the world what happened. That was notably true in move, and in the case of turn the world isn’t even informed at all.
So I made a tiny yellow card about that, and we’ll proceed accordingly. Perhaps tomorrow, perhaps Monday.
After taking too big a bite at the beginning, everything went quite smoothly, which makes me think we’re really in pretty good shape for plugging in our JSON. We also need to think about the socket connection a bit but I believe we can just create the JSON, send it, then hang on the receive on the Robot side. We might have to poll for it … I’m not sure what Codea would really do with a long delay. But since I don’t really plan to run this over the network, I’m not terribly worried.
Although … if the students, or Hill, ever built a running world server, it would be fun to connect this one to it.
That’s definitely not tomorrow or Monday.
See you one of those days, if all goes as I hope it will.