Let’s slide in the remaining callbacks, and see what else we can see before brekkers.

Yesterday we averted disaster by causing calls from Robot to World to use a callback to interpret the response from World. This ensures that we could make the request and response asynchronous, with a delay, if we needed to.

Naturally, this must become the way we always do it, and, quite likely, this will lead to some interesting evolution of the code’s design. We’ll see. Today, there are at least two more occasions to install the now-standard callback: scan/look, and launch.

I keep saying “scan/look” because the real command is “look”, but when I started this game, I thought it was “scan”, so the code says scan. Let’s start by fixing that.

There are 27 occurrences of the word “scan”, if I’ve counted them correctly, and it looks as if they all need changing. I’ll spare you the code. First thing, I’ll just see what happens if I global replace “scan” with “look”. It might just work.

Codea found 26. Entirely possible. Test. Tests are green, game works. Woot! Commit: rename all “scan” to “look”. I love it when a plan comes together.

Ah, found the 27th, it was capitalized and in a comment. Fixed that too.

Let’s do the launch next, because it is probably easier. “Look” does special processing.

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

Interesting in that this one doesn’t do what the others did:

function Robot:standardResponse(response)
    self:setResponse(response)
    self.knowledge = self.knowledge:newLensAt(self:x(), self:y())
end

The reason, I imagine, is that this code predates the Lens, and/or it is assuming that we’ll be started at (0,0), or it’s just plain wrong.

Kent Beck says that when faced with a hard change, first make the change easy and then make the easy change. This change isn’t very hard, but we can make it easier.

We have these bits of code now:

function Robot:back(steps)
    local callback = function(...) self:standardResponse(...) end
    self._world:back(self._name, steps, callback)
end

function Robot:forward(steps)
    local callback = function(...) self:standardResponse(...) end
    self._world:forward(self._name, steps, callback)
end

function Robot:turn(lOrR)
    local callback = function(...) self:standardResponse(...) end
    self._world:turn(self._name,lOrR, callback)
end

All three of these use the same callback, and all three define it. Can we remove this duplication somehow? Yes, like this:

function Robot:standardCallback()
    return function(...) self:standardResponse(...) end
end

function Robot:turn(lOrR)
    self._world:turn(self._name,lOrR, self:standardCallback())
end

And now we can change the others …

function Robot:back(steps)
    self._world:back(self._name, steps, self:standardCallback())
end

function Robot:forward(steps)
    self._world:forward(self._name, steps, self:standardCallback())
end

And we should be green. We are. Commit: remove duplication of callbacks using standardCallback method.

Now we can do launch like this:

function WorldProxy:launchRobot(name,robot)
    local kind = "RonRobot1"
    local shields = 99
    local shots = 99
    local request = {
        robot=name,
        command="launch",
        arguments={kind,shields,shots}
    }
    local responseDict = self.world:request(request)
    return Response(responseDict)
end

And …

function WorldProxy:launchRobot(name,robot, callback)
    local kind = "RonRobot1"
    local shields = 99
    local shots = 99
    local request = {
        robot=name,
        command="launch",
        arguments={kind,shields,shots}
    }
    local responseDict = self.world:request(request)
    callback(Response(responseDict))
end

Test. Green. Commit: launch uses standardCallback.

We’re left with the unique one, look. It looks like this:

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

function WorldProxy:look(...)
    local jsonString = self.world:look(...)
    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

The WorldProxy is helping us out here, by pulling apart the response that came back and creating a table of LookPackets, that the Robot then tucks away using its addLook method.

We could argue that the WorldProxy is being too helpful here, but it’s a dirty job and someone has to do it. Our mission right now is to do all this via callback, and at first glance it’s not clear that moving this functionality will make the change easier … and yet …

We clearly do not want to use the standard callback for look, but it does seem that what we want in the callback is all this looking stuff.

Make the change easy—or at least easier—by refactoring out the packet making. When I look more closely, I see that we have another issue in the way. We’re calling the world directly, on its look method. We first need to package all this up in a request and go through the request interface.

There’s a fair amount of code involved in requests, and in World:look. Let’s have a new heading.

Robot-World Look

function World:request(rq)
    ok, result = pcall(function() return self:requestProtected(rq) end)
    if ok then
        return result
    else
        return self:error("SERVER ERROR "..tostring(result))
    end
end

function World:requestProtected(rq)
    local robotName = rq.robot
    local c = rq.command
    local args = rq.arguments
    local response
    if c == "noResponse" then
        -- nothing
    elseif c == "launch" then
        response = self:launchRobot(robotName, args[1], args[2], args[3])
    elseif c == "forward" then
        response = self:forward(robotName, args[1])
    elseif c == "back" then
        response = self:back(robotName, args[1])
    elseif c == "turn" then
        response = self:turn(robotName, args[1])
    else
        response = self:error("No such command "..tostring(c))
    end
    if self:isValidResponse(response) then
        return response
    else
        return self:error("SERVER RETURNED NO RESPONSE TO "..tostring(c))
    end
end

function World:look(robotName)
    local robot = self._robots[robotName]
    local packets = {}
    if robot then
        local lens = self._knowledge:newLensAt(robot._x, robot._y)
        self:lookNSEWinto(packets, lens)
    end
    return self:jsonLookResult(packets)
end

We are not using a legal request, nor are we as yet creating a legal response.

Let’s see what our response looks like now, no pun intended.

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

Yeah, I don’t even know if this is a legal response packet, but I’m betting it isn’t. It’s surely lacking all kinds of things. Let’s see how they are built elsewhere:

function World:moveFB(robotName,step, steps)
    local robot = self._robots[robotName]
    local xStep,yStep = self:getSteps(robot._direction)
    robot._x = robot._x + xStep*step*steps
    robot._y = robot._y + yStep*step*steps
    local response = {
        result = "OK",
        data = {},
        state = self:robotState(robot)
    }
    return response
end

OK, we can start there.

function World:jsonLookResult(packetArray)
    local outcome = {
        result="OK",
        data = {},
        state=self:robotState(robot)
    }

This much tells me that I need to pass in the robot …

function World:jsonLookResult(robot, packetArray)
    local outcome = {
        result="OK",
        data = {},
        state=self:robotState(robot)
    }
    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

This should be harmless. Test and see. Good so far. Should I commit? In favor is that this is progress. Opposed is that I may screw up. I go with in favor, commit: Adding in legit outcome values in World:jsonLookResult.

I’d really like outcome to be named response to match the other code. Do that. Test and commit: rename local variable.

Now I think I have to review the documentation, which is a downer. I’m hoping that this response we’re building is in valid format. The document says that objects should be an array of direction, type, and distance. Our code in World is creating LookPacket instances, and they have direction, distance, and type:

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

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

Perfect. I believe that we are creating a legitimate Response object here, now that we’ve added in the extra stuff at the top.

So over in WorldProxy, we can be confident that we have a real response.

We have a couple of things we need to do. One is that we need to change the implementation of look to use the request/response form of code. The other is to change it to use a callback.

Let’s start with the request part and see what happens.

Use Request in Look

We change look thus:

function WorldProxy:look(name)
    local request = {
        robot=name,
        command="look",
        arguments={}
    }
    local jsonString = self.world:request(request)
    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

And expect that world will barf on look because it’s not in the dispatch. Yes, we get lots of serious errors:

2: Look on all four sides -- ...
EAF/Codea.app/Frameworks/RuntimeKit.framework/dkjson.lua:693: 
bad argument #2 to 'pegmatch' 
(string expected, got table)

We build look into the dispatch:

function World:requestProtected(rq)
    local robotName = rq.robot
    local c = rq.command
    local args = rq.arguments
    local response
    if c == "noResponse" then
        -- nothing
    elseif c == "launch" then
        response = self:launchRobot(robotName, args[1], args[2], args[3])
    elseif c == "look" then
        response = self:look(robotName)
    elseif c == "forward" then
        response = self:forward(robotName, args[1])
    elseif c == "back" then
        response = self:back(robotName, args[1])
    elseif c == "turn" then
        response = self:turn(robotName, args[1])
    else
        response = self:error("No such command "..tostring(c))
    end
    if self:isValidResponse(response) then
        return response
    else
        return self:error("SERVER RETURNED NO RESPONSE TO "..tostring(c))
    end
end

And test again. Still those same errors:

4: Robot does look/look on l key -- ...
EAF/Codea.app/Frameworks/RuntimeKit.framework/dkjson.lua:693: 
bad argument #2 to 'pegmatch' 
(string expected, got table)

Best look and see what that even is … a framework error. Oh my. Let’s revert and try again, I don’t even want to try to figure that out.

I can’t resist looking: Here’s a picture of all I changed:

working copy showing changes

I don’t know, revert, go in much smaller steps.

Revert, Smaller Steps

First thing that happens is that Codea comes up blank after the revert. I reboot the iPad. Test. Green. Change look to be more explicit about parameters. This is just lazy, done when I plugged in the proxy:

function WorldProxy:look(...)
    local jsonString = self.world:look(...)
    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

Calling, we have:

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

And so we can do this:

function WorldProxy:look(name)
    local jsonString = self.world:look(name)
    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. Green. Commit: explicit name parameter between Robot and WorldProxy look.

Now let me try the request route again.

function WorldProxy:look(name)
    local request = {
        robot=name,
        command="look",
        arguments={}
    }
    local jsonString = self.world:request(request)
    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

And this time, because superstition, I don’t run it until I get the command dealt with in World:

This time I notice that I’m returning a jsonString out of the look function. That could readily be the issue, since json is well inside Codea:

function World:requestProtected(rq)
    local robotName = rq.robot
    local c = rq.command
    local args = rq.arguments
    local response
    if c == "noResponse" then
        -- nothing
    elseif c == "launch" then
        response = self:launchRobot(robotName, args[1], args[2], args[3])
    elseif c == "look" then
        response = self:look(robotName)
    elseif c == "forward" then
        response = self:forward(robotName, args[1])
    elseif c == "back" then
        response = self:back(robotName, args[1])
    elseif c == "turn" then
        response = self:turn(robotName, args[1])
    else
        response = self:error("No such command "..tostring(c))
    end
    if self:isValidResponse(response) then
        return response
    else
        return self:error("SERVER RETURNED NO RESPONSE TO "..tostring(c))
    end
end

And in look we are still returning json:

function World:look(robotName)
    local robot = self._robots[robotName]
    local packets = {}
    if robot then
        local lens = self._knowledge:newLensAt(robot._x, robot._y)
        self:lookNSEWinto(packets, lens)
    end
    return self:jsonLookResult(robot, packets)
end

And …

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

We need to return the response, not the json thereof, so, for now:

function World:jsonLookResult(robot, packetArray)
    local response = {
        result="OK",
        data = {},
        state=self:robotState(robot)
    }
    local objects = {}
    for i,p in ipairs(packetArray) do
        table.insert(objects,p:asObject())
    end
    response.data = { objects=objects }
    return response
end

I think we can test. And again I get those nasty pegmatch messages. Revert again? No. I did a bit of printing. Then, with that telling me nothing, I typed l in the game and got an error I could understand, because the traceback pointed here:

function WorldProxy:look(name)
    local request = {
        robot=name,
        command="look",
        arguments={}
    }
    local jsonString = self.world:request(request)
    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

And there’s the bug! Since we’re getting a response back, not a string, we try to json decode a dictionary and apparently that’s not as good an idea as it might be. So the fix is:

function WorldProxy:look(name)
    local request = {
        robot=name,
        command="look",
        arguments={}
    }
    local outcome = self.world:request(request)
    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. Green. Commit: look now passes a request and gets a response back.

Back on track but I need a break and the cat is hungry,

Post Break

Well, we’re back on track. I wonder what I could have done that would have avoided that problem. My colleagues with type-checking languages would assure me that their language would have given me a compile error. That’s nice, but we are where we are, aren’t we? A better message from the json decode would have been nice.

Even after a bit of reflection, I don’t see a credible “should have”. I think the bear just bit me this time.

What about making that method smaller and what about tests for it?

function WorldProxy:look(name)
    local request = {
        robot=name,
        command="look",
        arguments={}
    }
    local jsonString = self.world:request(request)
    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

Let’s do refactor that: I think we’ll be glad we did anyway:

function WorldProxy:look(name)
    local request = {
        robot=name,
        command="look",
        arguments={}
    }
    local outcome = self.world:request(request)
    return self:makePackets(outcome)
end

function WorldProxy:makePackets(outcome)
    local packets = outcome.data.objects
    local result = {}
    for i,p in ipairs(packets) do
        local lp = LookPacket:fromObject(p)
        table.insert(result, lp)
    end
    return result
end

Somewhat better. Test. Green. Commit: refactor pulling out makePackets from WorldProxy:look.

Let’s remember what we’re up to. We’re trying to make the Robot-World “look” capability work like the others, including a callback. We will certainly want a special callback, because we expect WorldProxy to build our LookPackets for us and return them.

What we should do, I think, is have WorldProxy return us the response, as always, and deal with the details in Robot.

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

Let’s change the rules here. We’ll have WorldProxy return the result, and we’ll call it back to get the packets made, at least for now, because that will be a smaller step:

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

function WorldProxy:look(name)
    local request = {
        robot=name,
        command="look",
        arguments={}
    }
    return self.world:request(request)
end

I expect this to work. It does. Green: commit: Robot uses envious function in WorldProxy to get LookPackets during look command.

Now we can just move that method from WorldProxy to Robot:

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

function Robot:makePackets(outcome)
    local packets = outcome.data.objects
    local result = {}
    for i,p in ipairs(packets) do
        local lp = LookPacket:fromObject(p)
        table.insert(result, lp)
    end
    return result
end

Should be green. Is. Commit: Robot uses its own envious function to get look packets from response.

Feature envy: an object working on things it doesn’t even own.

Why envious? Because it has nothing to do with either Robot or WorldProxy. It looks like a class method on LookPacket to me. Let’s move it and let’s see if we need a test.

Yeah, we have like no tests for LookPacket (it is, after all, an object with no behavior):

        _:test("LookPacket", function()
            local packet = LookPacket("N", 5, "O")
            _:expect(packet:direction()).is("N")
            _:expect(packet:distance()).is(5)
            _:expect(packet:type()).is("O")
        end)

Let’s add a new test:

        _:test("Packets from Response",  function()
            local response= {
                data={
                    objects={
                        {direction="N", type="O", steps=5},
                        {direction="S", type="P", steps=3}
                    }
                }
            }
            local packets = LookPacket:fromResponse(response)
            _:expect(packets[1]:direction()).is("N")
            _:expect(packets[2]:direction()).is("S")
            _:expect(packets[1]:type()).is("O")
            _:expect(packets[2]:type()).is("P")
            _:expect(packets[1]:steps()).is(5)
            _:expect(packets[2]:steps()).is(3)
        end)

That will drive out the fromResponse method:

2: Packets from Response -- TestLookPacket:31: attempt to call a nil value (method 'fromResponse')

And:

function LookPacket:fromResponse(response)
    local objects = response.data.objects
    local result = {}
    for i,o in ipairs(objects) do
        table.insert(result, LookPacket:fromObject(o))
    end
    return result
end

I just went wild there. I expect green. I don’t get it because there is no method steps. Its name is distance. Fix the test.

        _:test("Packets from Response",  function()
            local response= {
                data={
                    objects={
                        {direction="N", type="O", distance=5},
                        {direction="S", type="P", distance=3}
                    }
                }
            }
            local packets = LookPacket:fromResponse(response)
            _:expect(packets[1]:direction()).is("N")
            _:expect(packets[2]:direction()).is("S")
            _:expect(packets[1]:type()).is("O")
            _:expect(packets[2]:type()).is("P")
            _:expect(packets[1]:distance()).is(5)
            _:expect(packets[2]:distance()).is(3)
        end)

Green. Commit: LookPacket has fromResponse method to make array of LookPackets.

Now use it:

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

Test expecting green. Green after trying three times to say “fromResponse”. Commit: Robot uses LookPacket class method fromResponse to make its packets.

Pause to reflect and to consider whether to continue.

Reflection

This is all good, though I am wondering about whether LookPacket is carrying its weight. We just seem to convert simple dictionaries back and forth to it. I think I’ll default to keep it, because using primitive data types is always kind of messy, so it may pay off later even if right now it’s pretty light.

We’re just left with the need to do the callback. And I think we should do this first:

function Robot:look()
    local response = self._world:look(self._name)
    local packets = LookPacket:fromResponse(response)
    for i,packet in ipairs(packets) do
        self:addLook(packet)
    end
    self:standardResponse(response)
end

I’ve added a call to standardResponse because it’s the right thing to do. We might even have been hit by an arrow or something, and it’s in standardResponse where that would be dealt with. Test. Error, and it’s a useful one:

4: Robot does look/look on l key -- TestRobot:202: attempt to call a nil value (method 'is_a')

We need our response to be a Response, not just a dictionary:

function Robot:look()
    local response = Response(self._world:look(self._name))
    local packets = LookPacket:fromResponse(response)
    for i,packet in ipairs(packets) do
        self:addLook(packet)
    end
    self:standardResponse(response)
end

This may break our fromResponse code so let’s enhance its test as well:

        _:test("Packets from Response",  function()
            local response= Response({
                data={
                    objects={
                        {direction="N", type="O", distance=5},
                        {direction="S", type="P", distance=3}
                    }
                }
            })
            local packets = LookPacket:fromResponse(response)
            _:expect(packets[1]:direction()).is("N")
            _:expect(packets[2]:direction()).is("S")
            _:expect(packets[1]:type()).is("O")
            _:expect(packets[2]:type()).is("P")
            _:expect(packets[1]:distance()).is(5)
            _:expect(packets[2]:distance()).is(3)
        end)

This does fail because we have a response now, so we must do this:

function LookPacket:fromResponse(response)
    local objects = response:dataObjects()
    local result = {}
    for i,o in ipairs(objects) do
        table.insert(result, LookPacket:fromObject(o))
    end
    return result
end

And this:

function Response:data()
    return self._r.data or {}
end

function Response:dataObjects()
    return self:data().objects or {}
end

And expect green. Green it is: Commit: Robot: look expects a response back, not a dictionary.

Finally, the callback:

Now where were we? Oh, yes, we want to change these to use a callback:

function Robot:look()
    local response = Response(self._world:look(self._name))
    local packets = LookPacket:fromResponse(response)
    for i,packet in ipairs(packets) do
        self:addLook(packet)
    end
    self:standardResponse(response)
end

function WorldProxy:look(name)
    local request = {
        robot=name,
        command="look",
        arguments={}
    }
    return self.world:request(request)
end

Change Robot to refactor out the callback:

function Robot:look()
    local response = Response(self._world:look(self._name))
    self:lookCallback(response)
end

function Robot:lookCallback(response)
    local packets = LookPacket:fromResponse(response)
    for i,packet in ipairs(packets) do
        self:addLook(packet)
    end
    self:standardResponse(response)
end

We could commit but let’s not. No, let’s do the right thing. Commit: refactor out lookCallback from Robot:look.

Now change to have WorldProxy call it:

function Robot:look()
    local callback = function(response) self:lookCallback(response) end
    self._world:look(self._name,callback)
end

function WorldProxy:look(name, callback)
    local request = {
        robot=name,
        command="look",
        arguments={}
    }
    local responseDict = self.world:request(request)
    callback(Response(responseDict))
end

Test. Green. Commit: Robot look uses callback mechanism. All commands converted.

Let’s sum up.

Summary

We started with the easy one, launch, and made the change easy and then made the easy change.

Look was not so simple and your engaging host messed it up a few times. The most confusing part was due to a simple type error, where I tried to json.decode a dictionary rather than a string.

It might be wise to cover the Codea JSON with our own little class and methods, where we could check the inputs for being strings and such. Might be worth it, at least for decode.

We had to do a number of steps, three or four at least, to convert “look” to the new callback scheme. In my defense, “look” was one of the first commands implemented, so it was a bit ragged. But then we have to ask ourselves, selves, why did we leave it ragged? Why didn’t we bring it up to standard?

I have no answer for that. We just didn’t. It was working and so we left it alone. Ain’t broke, don’t fix it. May have been the right thing to do since it wasn’t that hard to do today. What might have helped, though, would have been to bring all the commands up to snuff every time we redefine what snuff is, since then, each change would have been fresh in our minds, and smaller as well.

But we did what we did and the results are just fine. We reflect, not to beat ourselves up, but to try to beat into our heads some notions that will help us in future.

The main lesson for me, again and again, is small steps, and right on its heels is “test everything”.

What are your first couple of things you need to learn over and over?

See you next time!


P.S. Just noticed in the pic above: the last green line is the line that needed to be changed to stop that weird error. There is was, right in front of me. What could I have done to notice that a string wasn’t going to come back any more? I’m not sure. But there it was, ready to be spotted. Could have saved a few minutes if I were more clever, but if I were more clever, I’d have more money and an even better car. Same cat, though, probably.