Robot 23
Today we work further toward accommodating the ‘spec’. I think we’ll focus on the robot state. I think I may rediscover why something is a good idea.
I think it is a fundamental design notion of the original Robot Worlds spec that the World maintains the Robot’s “true” status. The World knows where it is, which way it is facing, whether or not it has fallen into a pit, and so on. While it is explicitly allowed for the Robot to keep track of any information we may wish to track, the World’s information is “the truth”. Requests always return robot state, which is a dictionary kind of thing like this:
{
...
state={
position=[x,y],
direction="N",
shields="number of hits that can be absorbed",
shots="number of shots left",
status="operational status"
}
}
We’ll encounter the details about these as we go forward. The state
object is itself embedded in the “Response Message”, which also includes result
, and data
. We’ve seen a bit of data
and will see more as we progress.
This morning, I think I’d like to change my Robot so that it retains, and uses, the information in the state
when it needs to know things about itself. To begin, I would like for state to be kept, not in a raw dictionary thing, but in an object with the same shape. Let’s TDD RobotState. Shouldn’t need much.
RobotState
I create a new tab “TestRobotState”, and paste in and edit my standard test starter. It runs green. Now to test. The RobotState object might as well be created with a dictionary, because that’s what we’ll be getting back from the World server.
_:test("RobotState", function()
local stateDict = {}
local state = RobotState(stateDict)
end)
This is more than enough to drive out the class:
1: RobotState -- TestRobotState:17: attempt to call a nil value (global 'RobotState')
And …
RobotState = class()
Nota Bene: I know that I’ll benefit from smaller steps. So even the test above is more than I needed to drive out the class. The second line alone would have done it. I’ll try to go in tiny steps, but I’ll try not to get silly about it.
I want my RobotState to just contain the dictionary, and to have accessors to fetch information that the Robot needs. Should I provide for all of position, direction, shields, shots, and status now? I have no need of them. Still, I want to get started. Let’s do position. It’s speculative, but not very.
_:test("RobotState", function()
local stateDict = {
position={1,2}
}
local state = RobotState(stateDict)
local x,y = state:position()
_:expect(x).is(1)
_:expect(y).is(2)
end)
That should suffice to push out some code. Test will fail looking for position.
1: RobotState -- TestRobotState:20: attempt to call a nil value (method 'position')
OK, in service of taking the smallest steps:
function RobotState:position()
end
Failure will be nils for x and y.
1: RobotState --
Actual: nil,
Expected: 1
1: RobotState --
Actual: nil,
Expected: 2
I don’t see anything for it but two do this much:
function RobotState:init(aDictionary)
self._dict = aDictionary
end
function RobotState:position()
return self._dict.position[1],self._dict.position[2]
end
I suppose I could have returned vanilla 1,2 in position, then plugged in the dictionary … I think this was tiny enough.
There is the question of whether we should check to see that everything is legitimate. There certainly are places where things could go wrong, between the World creating an incorrect dictionary, to something going wrong in the JSON, and so on. Because it comes to us from the outside, I do think we should validate it. That’ll be somewhat interesting. Let’s do it.
We already have a conveniently invalid input dictionary, the one in our test. I’d rather work with another test. We’ll perhaps want to ignore
this test. We’ll see. A new test:
_:test("Invalid States", function()
local stateDict
t = function() RobotState(stateDict) end
_:expect(t).throws("no dictionary")
end)
We have to create the function t because we want the call to create the RobotState to occur when throws
asks for it. An oddity of CodeaUnit and Lua.
Test:
1: Invalid States --
Actual: nothing thrown,
Expected: no dictionary
No surprise.
function RobotState:init(aDictionary)
self._dict = aDictionary
self:validate()
end
function RobotState:validate()
assert(self._dict, "no dictionary")
end
I expect a pass. Yes. Now make the test harder:
_:test("Invalid States", function()
local stateDict
t = function() RobotState(stateDict) end
_:expect(t).throws("no dictionary")
stateDict = {}
_:expect(t).throws("no position")
end)
I did position first just because I knew it was necessary. Should probably really do them in alpha order or something.
Test will fail to throw.
1: Invalid States --
Actual: nothing thrown,
Expected: no position
Fix:
function RobotState:validate()
assert(self._dict, "no dictionary")
assert(self._dict.position, "no position")
end
Tests green. Extend:
stateDict.position = 3
_:expect(t).throws("position must be two entry array")
Test will fail, nothing thrown:
1: Invalid States --
Actual: nothing thrown,
Expected: position must be two entry array
Implement:
Grr. I ran into some misunderstanding. The returns from the throws
calls aren’t always clear, because if you throw with the wrong message you see something like this:
3: must be array --
Actual: function: 0x2840d82a0,
Expected: position must be two element array
That threw me off track, and I decided to write the tests out one at a time.
So we’re here:
_:test("dictionary", function()
local stateDict
local t = function() RobotState(stateDict) end
_:expect(t).throws("no dictionary")
end)
_:test("position", function()
local stateDict
local t = function() RobotState(stateDict) end
stateDict = {}
_:expect(t).throws("no position")
end)
_:test("must be array", function()
local stateDict
local t = function() RobotState(stateDict) end
stateDict = {}
stateDict.position = 3
_:expect(t).throws("position must be two element array")
end)
_:test("two elements", function()
local stateDict
local t = function() RobotState(stateDict) end
stateDict = {}
stateDict.position = {1}
_:expect(t).throws("position must be two element array")
end)
_:test("elements", function()
local stateDict
local t = function() RobotState(stateDict) end
stateDict = {}
stateDict.position = {1,2,3}
_:expect(t).throws("position must be two element array")
end)
And validate
is implemented like this, so far:
function RobotState:validate()
assert(self._dict, "no dictionary")
local p = self._dict.position
assert(p, "no position")
local t = type(p)
assert(t=="table", "position must be two element array")
assert(#t==2, "position must be two element array")
end
This is tedious. And my correct test is throwing the array error
I finally spot my bug in the last line, checking t, the type, instead of p, the position.
function RobotState:validate()
assert(self._dict, "no dictionary")
local p = self._dict.position
assert(p, "no position")
local t = type(p)
assert(t=="table", "position must be two element array")
assert(#p==2, "position must be two element array")
end
That’s what I get for rushing with short names. Tests green. Rename:
function RobotState:validate()
assert(self._dict, "no dictionary")
local position = self._dict.position
assert(position, "no position")
local pType = type(position)
assert(pType=="table", "position must be two element array")
assert(#position==2, "position must be two element array")
end
I’d never have typed
assert(#type==2, ...)
Not in a million years. This is still tedious, however. Is it necessary? Well, we can guarantee that in our own code we’ll probably return an invalid state at some point, I’m just not that clever. So let’s see if we can see how to make this less tedious, or at least more clear.
Let’s try extracting …
function RobotState:validate()
assert(self._dict, "no dictionary")
self:validatePosition()
end
function RobotState:validatePosition()
local position = self._dict.position
assert(position, "no position")
local pType = type(position)
assert(pType=="table", "position must be two element array")
assert(#position==2, "position must be two element array")
end
As soon as I do this, I see one nice thing about breaking the validations out: I can implement empty ones and fill them in later, providing the right structure while leaving the details for later.
And I’d like to have the errors all produced by the main validate
function. Like this:
function RobotState:validate()
assert(self._dict, "no dictionary")
assert(self:validatePosition(), "position must be array [x,y]")
end
function RobotState:validatePosition()
local position = self._dict.position
return position and type(position)=="table" and #position==2
end
Wow. I did not see that coming. Good news. Once I started just returning true/false, that nice implementation dropped out. So I changed all the expectations to the better message, and we’re green again.
I should commit: Initial RobotStatus checks position in input dictionary.
This has taken me rather longer than I had anticipated. I think I started just before ten and it is almost 1130 now and this is my first commit. I could have done more, but this is arguably the first reasonable time to commit.
Relearning is still learning.
Well, it takes as long as it takes, and I was troubled by the #t
that should have been #p
for longer than I care to admit. I do love how I learn so quickly not to take shortcuts, over and over again. It’s weird how we know what we should do and then we do something else, isn’t it? Where did I put those potato chips?
I’d really like to plug this status into Robot today. Let’s at least take a look at how that would go.
Robot with Status
Robot gets created like this:
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
If launchRobot were kind enough to return a status dictionary, we could plug it in right there where we do x and y. We’ll have to worry about x and y of course.
I’m trying to channel what Hill would do, how he would find a way to do this one line at a time. Let’s look at our proxy. We might fake it there and then proceed.
function WorldProxy:launchRobot(...)
return self.world:launchRobot(...)
end
Now we know that we’re just getting back x and y, and if we didn’t know that we could look, so we can do this:
function WorldProxy:launchRobot(...)
local x,y = self.world:launchRobot(...)
return x,y
end
Tests should be green. They are. Now I am really sure that I have to change more than one line. Let’s make a dictionary in the proxy launch:
function WorldProxy:launchRobot(...)
local x,y = self.world:launchRobot(...)
local status = {
position = {x,y}
}
return status
end
And in Robot:
function Robot:init(name,aWorld)
assert(aWorld:is_a(WorldProxy), "expected WorldProxy")
self._world = aWorld
self._status = aWorld:launchRobot(name, self)
self._x, self._y = self._status.position[1], self._status.position[2]
self._name = name
self.knowledge = Knowledge()
end
Green. Could commit. Will, just to prove it: Proxy launch returns status dictionary, only position so far.
Now we have a position method in RobotStatus, we started with that, so let’s refactor to this:
Wow. Another really odd mistake. I named the new class RobotState. And, it turns out, we already have a nascent class, RobotStatus, in WorldProxy. I used that and of course it didn’t work. First make it work, using the new object.
function Robot:init(name,aWorld)
assert(aWorld:is_a(WorldProxy), "expected WorldProxy")
self._world = aWorld
self._status = RobotState(aWorld:launchRobot(name, self))
self._x, self._y = self._status:position()
self._name = name
self.knowledge = Knowledge()
end
We are green. Commit again: Using RobotState class in Robot. Needs convergence with RobotStatus!!!
OK, what’s up with RobotStatus? Did someone already start working on this idea?
Oh yes, the World has one:
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
How should we converge these two ideas? Perhaps we need not do so at all. Arguably World should have its own view of the robot’s status and so should the Robot. In a real situation we wouldn’t be building both sides all in one project. That’s an artifact of the Codea situation.
The two things will be mirrors of each other, since they share a common spec. For now, I’m going to make RobotStatus local to World.
Now on the other side, the Robot side, let’s rename RobotState simply to Status. It is, after all, the only status we care about.
Global replace doesn’t kill me. Still green. Commit: Robot Status object is Status
. World has local class RobotStatus.
Where Were We … Oh, Yes …
We were working toward getting the Robot’s status from that of the World. Presently we’re creating it in the Proxy:
function WorldProxy:launchRobot(...)
local x,y = self.world:launchRobot(...)
local status = {
position = {x,y}
}
return status
end
Let’s review the launch in World:
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
Well, if we were to return a properly formatted status here, that would be sweet. What does the spec say1?
Well, it says a lot. The general response packet has result, data, and state. There’s a lot of data. Let’s first code enough in line to have the right format.
function World:launchRobot(name, kind,shieldStrength, shots)
local robot = RobotStatus(self, name, kind, shieldStrength, shots)
self._robots[name]=robot
local response = {
result="OK",
data={},
state={
position = {robot._x, robot._y},
direction = "N",
status = "NORMAL",
shots = robot._shots,
shields = robot._strength
}
}
return response
end
I kind of went over the top here. Hill would be screaming. On the Proxy side:
function WorldProxy:launchRobot(...)
local response = self.world:launchRobot(...)
return response.state
end
We are green. Commit: Enough response returned from World:launch to keep skeleton walking.
I notice that my notion of state vs status is fuzzy, and that the spec has an opinion. The Robot’s “state” is the larger object with position and all that, and its “status” is NORMAL, DEAD, and so on. Let’s rename RobotStatus and Status to RobotState and State.
Green. Commit: Rename RobotStatus to RobotState, and Status to State.
I think we could draw a line under the day right here, but I want first to look at the places where we did the JSON stuff. There are a few, of the nature of experimental first steps:
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
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
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
Those are the most significant ones. I have a sort of design, sketched in fake pencil, for how this thing should ultimately work:
That scrawl says “Maybe RobotStatus is more prominent”.
I envision that the robot thinks of things it wants to say to the world and says them to the WorldProxy. The Proxy converts each saying to a Request and passes it to an object that converts it to JSON and sends it to the World. That object will receive the response, in JSON, from the World, convert it to a dictionary, and return it to the Proxy, which converts it to a Result and returns that to the Robot, which will unpack whatever data it cares about, store the state part, and so on, all to be figured out one day soon.
On the world side it is similar, there is a receiver that sees some JSON and decodes it into a dictionary, which it sends to an object that turns it into a Request, and sends it to the World, which deals with the Request, creates a Response, returns it to the JSON thingie, which converts it to JSON and sends it on its way.
In the middle, there’s a socket connection.
Je Ne Regrette Rien
I don’t regret starting with direct calls: I think they are a better structure for my main objects. But I am certainly aware that the current structure of the overall program needs some conversion layers put in, and if I were a person who did regret, I’d probably be wishing that I had had more of an understanding of the spec than I had when I started, or that I were smarter, or that someone had asked me about a Command Pattern sooner or something … you begin to see why I try not to do regret: I’d run out of time to do anything else.
We have some work to do, we’ll leave it at that. I think we’d do well to continue on the path of getting these request and response objects / dictionaries in place for the commands we have now.
We’ll continue with that tomorrow, working closer and closer to round trip with these command / response dictionaries … which we’ll make be true, albeit not very smart objects, as we’ve begun with State and RobotState.
See you then!
-
The fox says “Wa pa pa pa pow”. As for the spec … ↩