Robot 44
I wonder why I’m doing what I’m doing. Meanwhile, requirements changes to the robot’s radar result in some design issues.
If you’re reading this, you are in a very small group. These articles seem to be getting very low readership. I don’t mind that, but I’d prefer more readers, because I imagine that frequent readers would be getting some value, and I want to provide something of value.
So, if you find value here, maybe share with your friends. And if you have ideas for what would be of more value to you, drop me a tweet or something. I don’t promise to do as you suggest, but I might, and I’ll certainly try to be of more value.
Anyway, today I plan to work for a bit on the remaining aspects of our new radar look scanning whatever it is:
The look command will examine all cells withinvisibility
distance of the robot;- Each move will automatically return look results, like a radar that is always scanning;
- Enhance the look command to return a special result for “shadowed” cells, that is, those that are blocked from the radar by an impenetrable obstacle.
I think that the automatic look will be easy, in that we’ll probably just have to make a call to our look function at the end of all the other commands. So I’m not concerned about that.
The shadowing raises more concerns. First, our current implementation of the light areas that show where we have looked is done on the client side: we add a “LOOK” fact to the robot’s Knowledge for every cell in the look circle. It seems more reasonable for the World radar to return that information and to include “SHADOW” as well as “LOOK”.
Second, we’re already adding more and more redundant facts to the Knowledge, and shadowing will do even more, since a given cell will be scanned many times and may change state. It seems that we need for our Knowledge object to keep only one element per cell, or at least not arbitrarily many.
I think we can defer the second issue for a bit longer. Our LIFO approach to storing Facts means that we see the most recent entry for any cell, so that while the table may be growing profoundly, the picture stays right.
Except … hmm … suppose on one look we see an Obstacle, but then on a later look from a different position, that Obstacle is in the shadow. Our Robot’s radar remembers where it has seen things and displays them even if they are outside the current scan:
In the picture above, the obstacles and pits at the bottom of the screen are outside scanning range. In fact, I’ve only done the one scan that revealed them. They are remembered by the radar system in my Robot. The issue here is that if we were to scan from another position and shadow one of those items, we don’t want to override an actual scan with shadow.
Maybe this is OK. I had thought to provide a different color for shadow, but if instead we just stop scanning, we can return empty cells and occupied cells from the look, and just not return the shadowed ones. That might look fine.
Note, however, that we’re going to have to stop with our trick of marking cells as “LOOK” from the robot side, and rely on the World to provide them.
Let’s get to it. Some of these issues will become more clear as we work.
Shadowing
In cell-based games, like this one, the notion of “Field of View” is common. If there’s a column in the dungeon, a monster might be behind it, but if it isn’t in your field of view, you won’t see it (and generally, it won’t see you).
There are a number of algorithms for computing field of view. As it happens, I have code for one of them, and I plan to reuse it. It works like this:
Starting from where you are, draw a line from there to each of the boundary cells around you at the limit of your vision. As you move along that line, you can see anything in the cells that line goes through, until you see an obstacle. You can’t see anything after that, along that line.
It looks a bit like this:
There’s an issue: since the world is cells, the “lines” we draw may traverse a given cell more than once. Imagine that there are 44 cells around us at distance 8, which I believe to be the case. There are only 9 cells right adjacent to us, so our 44 lines are going to encounter those cells four or five times. We’ll want to avoid storing that redundant information … although, again, with our LIFO storage, it will probably work, at least until the iPad fills up with Facts.
I propose a spike to see what happens.
First Shadowing Spike
I call this “first” because I think it may not be the last.
First, let’s turn off the code that puts in the LOOK cells on the robot side.
function Robot:look()
--self:markCellsLookedAt(self.worldinfo.visibility)
local callback = function(response) self:lookCallback(response) end
self._world:look(self._name,callback)
end
That’s easy. A couple of checks fail, the ones that were reflecting results from that.
Now I want to import my line drawing code. I wish I had done that first, because I can surely commit it, since it won’t do anything.
-- Bresenham
-- RJ 20201218
-- from http://rosettacode.org/wiki/Bitmap/Bresenham%27s_line_algorithm#Lua
Bresenham = class()
function Bresenham:drawLine(x1, y1, x2, y2, c)
local coords = {}
local dx, sx = math.abs(x2-x1), x1<x2 and 1 or -1
local dy, sy = math.abs(y2-y1), y1<y2 and 1 or -1
local err = math.floor((dx>dy and dx or -dy)/2)
while(true) do
table.insert(coords, vec2(x1,y1))
if (x1==x2 and y1==y2) then break end
if (err > -dx) then
err, x1 = err-dy, x1+sx
if (x1==x2 and y1==y2) then
table.insert(coords,vec2(x1,y1))
break
end
end
if (err < dy) then
err, y1 = err+dx, y1+sy
end
end
return coords
end
In fact, I will commit this and not the commenting thing: Added harmless Bresenham function.
We see by a quick inspection that the drawLine function, given starting and stopping x and y, returns a table of all the cells looked at. (I have no idea what that parameter c is about, and it isn’t used. An appendix or something. We’ll pretend that never happened.
Now let’s see how we do the look right now:
function World:look(robotName)
local robot = self._robots[robotName]
local packets = {}
if robot then
local lens = self._knowledge:newLensAt(robot._x, robot._y)
self:lookCircularIntoPackets(packets, lens)
end
return self:jsonLookResult(robot, packets)
end
function World:lookCircularIntoPackets(packets,lens)
local v = self.constants.visibility
for x = -v,v do
for y = -v,v do
if vec2(x,y):len() <= v then
self:lookFactInto(packets, lens, x,y)
end
end
end
end
Picture the square in which our circle of radius v resides. Its top can be thought of as ranging from -v to v at y=v, and its bottom is -v to v at y=-v. The sides, since we’ve already done the corners, go from -v+1 to v-1 at x=v and x=-v. If we draw our imaginary sight lines to those positions, we’re sure to have tried to look at every cell in range at least once.
Let’s TDD a little something. My idea is this:
- Create a table of targets, the boundary cells at distance d;
- For each target, get the cells on the Bresenham line to that target, and process them to create our Facts.
The second part is admittedly more vague than the first, but we’ll get there.
I might as well add this to my Bresenham class:
_:test("target cells", function()
local targets = Bresenham:targets(1)
_:expect(#targets).is(9)
end)
I happen to be able to visualize the nine cells around the center, and this should be enough to get us moving. Test will fail looking for targets method.
1: target cells -- Bresenham:42: attempt to call a nil value (method 'targets')
And code:
LOL. The test is wrong. There are 8 cells around 0,0, not 9. So much for my vaunted visualization. Anyway, the code:
function Bresenham:targets(d)
local targets = {}
for x = -d,d do
table.insert(targets, vec2(x,d))
table.insert(targets, vec2(x,-d))
end
for y = -(d-1),d-1 do
table.insert(targets, vec2(d,y))
table.insert(targets, vec2(-d,y))
end
return targets
end
I could interrogate the values. There are only 8 of them.. I’m really confident but I’ve already made two mistakes (the other was in the y
line, where I wrote -d+1,d+1
to begin with.
_:test("target cells", function()
local targets = Bresenham:targets(1)
_:expect(#targets).is(8)
_:expect(targets).has(vec2(-1,1))
_:expect(targets).has(vec2(0,1))
_:expect(targets).has(vec2(1,1))
_:expect(targets).has(vec2(-1,-1))
_:expect(targets).has(vec2(0,-1))
_:expect(targets).has(vec2(1,-1))
_:expect(targets).has(vec2(1,0))
_:expect(targets).has(vec2(-1,0))
end)
That passes. Just for fun. At distance 8 there will be um 17 and top and bottom and 15 on the sides, for 64 cells?
_:test("target cells size 8", function()
local targets = Bresenham:targets(8)
_:expect(#targets).is(64)
end)
That passes. Interesting power of two in there. Tempted to explore that. But we have lines to draw. We can commit the Bres tab: Bres:targets() produces list of targets on square of “radius” d.
I think I’d like to TDD a new method for World. If I’m careful, I can do that as long as I don’t use it too soon. I’m going to revert out my change to Robot and see if we can do this in one go.
Reverted. We are now green, with Bres stuff included.
Line Scan Method
I’d like to work top-down starting from look
but if I go bottom up maybe I can TDD a new line-scanning look feature. If I can’t, I’ll just drop back into spike mode.
My basic plan for the new look is to get the targets and then for each target, walk along the line from zero to the target, and spit out LOOK Facts when I don’t have a real Fact, Pits when I have a Pit, and Obstacle when I have an Obstacle. When I hit an obstacle (but not a Pit), I’ll stop the walk.
I think we can do any line we want, they’ll all be processed the same. So …
_:test("look along one line", function()
local world = World(25,25)
world:addPit(0,3, 0,3)
world:addObstacle(0,5,0,5)
local target = vec2(0,8)
local facts = world:walkLine(target)
_:expect(#facts).is(5)
end)
I expect the result to be L1, L2, P3, L4,O5, in a shorthand I just invented. I wonder about the zero cell. I’m sure it’s included in the drawLine
output. We’ll see.
This will fail looking for walkLine,
27: look along one line -- TestWorld:622: attempt to call a nil value (method 'walkLine')
Implement:
function World:walkLine(vec)
local facts = {}
return facts
end
Cleverly, I avoid my standard error of forgetting to return the result. Fail on the count.
27: look along one line --
Actual: 0,
Expected: 5
Sweet. Now we need to do some work. We’ll get the cells and then use them:
function World:walkLine(vec)
local facts = {}
local cells = Bresenham:drawLine(0,0, vec.x, vec.y)
table.remove(cells,1) -- take out the 0,0
for _i,v in ipairs(cells) do
local fact = self:newLookFactAt(v.x, v.y)
table.insert(facts, fact)
if fact.content == "OBSTACLE" then break end
end
return facts
end
I’m positing a new method newLookFactAt
. Awkward name will not survive the purge, but I want something different from what we already have.
I realize that I need a lens. A bit of coding off the top of my head …
function World:walkLine(vec)
local lens = self._knowledge:newLensAt(0,0)
local facts = {}
local cells = Bresenham:drawLine(0,0, vec.x, vec.y)
table.remove(cells,1) -- take out the 0,0
for _i,v in ipairs(cells) do
local fact = self:newLookFactAt(lens, v.x, v.y)
table.insert(facts, fact)
if fact.content == "OBSTACLE" then break end
end
return facts
end
function World:newLookFactAt(lens,x,y)
local fact = lens:factAt(x,y) or "LOOK"
return Fact(x,y, fact)
end
This is assuming a robot at 0,0 but I think the test might run and if not it’ll be interesting.
27: look along one line -- TestWorld:644: attempt to call a nil value (global 'Fact')
Let’s return a simple struct for now. Fact class doesn’t belong to us.
function World:newLookFactAt(lens,x,y)
local fact = lens:factAt(x,y) or "LOOK"
return {x=x, y=y, fact=fact}
end
Test.
27: look along one line --
Actual: 8,
Expected: 5
Looking for content and I stored fact:
function World:newLookFactAt(lens,x,y)
local fact = lens:factAt(x,y) or "LOOK"
return {x=x, y=y, content=fact}
end
I have high hopes for this. The test passes. Shall we check the items?
_:test("look along one line", function()
local world = World(25,25)
world:addPit(0,3, 0,3)
world:addObstacle(0,5,0,5)
local target = vec2(0,8)
local facts = world:walkLine(target)
_:expect(#facts).is(5)
_:expect(facts[1].content).is("LOOK")
_:expect(facts[2].content).is("LOOK")
_:expect(facts[3].content).is("PIT")
_:expect(facts[4].content).is("LOOK")
_:expect(facts[5].content).is("OBSTACLE")
end)
I expect green. I get green. We can commit this, since no one is using our new functions: new methods walkLine and newLookFactAt tested but unused.
This is going nicely. Let’s review the existing look code again.
Existing Look
function World:look(robotName)
local robot = self._robots[robotName]
local packets = {}
if robot then
local lens = self._knowledge:newLensAt(robot._x, robot._y)
self:lookCircularIntoPackets(packets, lens)
end
return self:jsonLookResult(robot, packets)
end
function World:lookCircularIntoPackets(packets,lens)
local v = self.constants.visibility
for x = -v,v do
for y = -v,v do
if vec2(x,y):len() <= v then
self:lookFactInto(packets, lens, x,y)
end
end
end
end
function World:lookFactInto(packets, lens, x,y)
table.insert(packets, self:createPacketFor(lens,x,y))
end
function World:createPacketFor(lens,x,y)
local content = lens:factAt(x,y)
if not content then return nil end
return LookPacket:fromXY(content,x,y)
end
What I need to do now includes:
- Return LookPackets from my newLookFactAt method, or from someone who calls that;
- Loop over all Bres targets, calling my new walkLine method, getting lots of LookPackets;
- Probably, deal with the proliferation of redundant LookPackets.
Let’s do #1.
function World:walkLine(vec)
local lens = self._knowledge:newLensAt(0,0)
local packets = {}
local cells = Bresenham:drawLine(0,0, vec.x, vec.y)
table.remove(cells,1) -- take out the 0,0
for _i,v in ipairs(cells) do
local packet = self:newLookPacketAt(lens, v.x, v.y)
table.insert(packets, packet)
if packet._type == "OBSTACLE" then break end
end
return packets
end
function World:newLookPacketAt(lens,x,y)
local fact = lens:factAt(x,y) or "LOOK"
return LookPacket:fromXY(fact,x,y)
end
Now I’ve got look packets, and I hate that LookPackets have a _type
member while the program thinks content
all over. I think that type
is the key word in the spec (ptui!).
OK, walkLine
is returning packets.
I want a method walkAllLines
. Let’s see what we can TDD of that.
_:test("walk all lines", function()
local world = World(25,25)
local packets = world:walkAllLines()
_:expect(#packets).is(3)
end)
It’s not going to be 3, it’s going to be a lot. But this will let me code it:
function World:walkAllLines()
local packets = {}
local targets = Bresenham:targets(self.constants.visibility)
for _i,t in ipairs(targets) do
local packs = self:walkLine(t)
for _j,p in ipairs(packs) do
table.insert(packets, p)
end
end
return packets
end
The result is interesting:
28: walk all lines --
Actual: 512,
Expected: 3
Recall that there are 2*17+2*15 target cells, or 64. Each Bresenham line is guaranteed to take 8 steps for a line of length 8, and 8*64 is 512. So this is the right number.
I’m still not using any of this, and brekkers is near. Let’s commit: method walkAllLines tested and returns mass quantities of cells. Still unused.
Now to spike. Let’s plug that walkAllLines method into look.
Look looks like this:
function World:look(robotName)
local robot = self._robots[robotName]
local packets = {}
if robot then
local lens = self._knowledge:newLensAt(robot._x, robot._y)
self:lookCircularIntoPackets(packets, lens)
end
return self:jsonLookResult(robot, packets)
end
I’ll make an override version:
function World:look(robotName)
local robot = self._robots[robotName]
local packets = self:walkAllLines()
return self:jsonLookResult(robot, packets)
end
That produces this picture on the first look command:
The view is square, because we’re not limiting on lie length yet. But the shadow looks perfect. I think we’re in a good place. I can’t commit quite yet, but after brekkers I’ll see about a quick wrapup.
For now: bacon!
Later That Day …
OK, what’s not to like? I think the main thing is that the new look code isn’t sensitive to the robot location. We need to pass it down. Our current code is this:
function World:look(robotName)
local robot = self._robots[robotName]
local packets = self:walkAllLines()
return self:jsonLookResult(robot, packets)
end
function World:walkAllLines()
local packets = {}
local targets = Bresenham:targets(self.constants.visibility)
for _i,t in ipairs(targets) do
local packs = self:walkLine(t)
for _j,p in ipairs(packs) do
table.insert(packets, p)
end
end
return packets
end
function World:walkLine(vec)
local lens = self._knowledge:newLensAt(0,0)
local packets = {}
local cells = Bresenham:drawLine(0,0, vec.x, vec.y)
table.remove(cells,1) -- take out the 0,0
for _i,v in ipairs(cells) do
local packet = self:newLookPacketAt(lens, v.x, v.y)
table.insert(packets, packet)
if packet._type == "OBSTACLE" then break end
end
return packets
end
function World:newLookPacketAt(lens,x,y)
local fact = lens:factAt(x,y) or "LOOK"
return LookPacket:fromXY(fact,x,y)
end
Rather than create the lens in walkLine
, we need to pass it in from look
. Let’s just do that and fix the tests that break.
function World:look(robotName)
local robot = self._robots[robotName]
local lens = self._knowledge:newLensAt(robot._x, robot.y)
local packets = self:walkAllLines(lens)
return self:jsonLookResult(robot, packets)
end
function World:walkAllLines(lens)
local packets = {}
local targets = Bresenham:targets(self.constants.visibility)
for _i,t in ipairs(targets) do
local packs = self:walkLine(lens, t)
for _j,p in ipairs(packs) do
table.insert(packets, p)
end
end
return packets
end
function World:walkLine(lens, vec)
local packets = {}
local cells = Bresenham:drawLine(0,0, vec.x, vec.y)
table.remove(cells,1) -- take out the 0,0
for _i,v in ipairs(cells) do
local packet = self:newLookPacketAt(lens, v.x, v.y)
table.insert(packets, packet)
if packet._type == "OBSTACLE" then break end
end
return packets
end
I expect this to work right in world. Test. Curiously it does not. This happens when I hit the look key:
TestResponse:110: attempt to index a nil value (field 'state')
stack traceback:
TestResponse:110: in function <TestResponse:109>
(...tail calls...)
TestRobot:258: in method 'standardResponse'
TestRobot:231: in method 'lookCallback'
TestRobot:225: in local 'callback'
TestWorldProxy:86: in method 'requestWithCallback'
TestWorldProxy:72: in method 'look'
TestRobot:226: in method 'look'
TestRobot:200: in method 'keyboard'
Main:22: in function 'keyboard'
And it happens again in draw
.
This is one of the few times I’ve ever wished I was working on a branch. I need to undo those changes and see what’s going on. I can do it manually, of course …
I don’t see what’s wrong.
I redo it carefully:
function World:look(robotName)
local lens, packets
local robot = self._robots[robotName]
if robot then
lens = self._knowledge:newLensAt(robot._x, robot._y)
else
lens = self._knowledge:newLensAt(0,0)
end
packets = self:walkAllLines(lens)
return self:jsonLookResult(robot, packets)
end
function World:walkAllLines(lens)
local packets = {}
local targets = Bresenham:targets(self.constants.visibility)
for _i,t in ipairs(targets) do
local packs = self:walkLine(lens, t)
for _j,p in ipairs(packs) do
table.insert(packets, p)
end
end
return packets
end
function World:walkLine(lens, vec)
local packets = {}
local cells = Bresenham:drawLine(0,0, vec.x, vec.y)
table.remove(cells,1) -- take out the 0,0
for _i,v in ipairs(cells) do
local packet = self:newLookPacketAt(lens, v.x, v.y)
table.insert(packets, packet)
if packet._type == "OBSTACLE" then break end
end
return packets
end
function World:newLookPacketAt(lens,x,y)
local fact = lens:factAt(x,y) or "LOOK"
return LookPacket:fromXY(fact,x,y)
end
The world picture is now right. I do not see what was different. I have broken tests. No surprise there, I changed the protocol.
27: look along one line -- TestWorld:664: attempt to index a nil value (local 'vec')
_:test("look along one line", function()
local world = World(25,25)
world:addPit(0,3, 0,3)
world:addObstacle(0,5,0,5)
local target = vec2(0,8)
local lens = world._knowledge:newLensAt(0,0)
local packets = world:walkLine(lens, target)
_:expect(#packets).is(5)
_:expect(packets[1]._type).is("LOOK")
_:expect(packets[2]._type).is("LOOK")
_:expect(packets[3]._type).is("PIT")
_:expect(packets[4]._type).is("LOOK")
_:expect(packets[5]._type).is("OBSTACLE")
end)
I expect adding the lens to fix this test. It does. Next is:
28: walk all lines -- TestWorld:676: attempt to index a nil value (local 'lens')
This is probably a similar issue.
_:test("walk all lines", function()
local world = World(25,25)
local lens = world._knowledge:newLensAt(0,0)
local packets = world:walkAllLines(lens)
_:expect(#packets).is(512)
end)
I expect this test to work now. It does. Two more failures:
1: Robot updates knowledge on move exists not yet visible --
Actual: not visible on first look,
Expected: nil
1: Robot updates knowledge on move --
Actual: not visible on first look,
Expected: nil
I think this will be because we’re not limiting the range of our Bresenham walks. The test is:
_:test("Robot updates knowledge on move", function()
local world = WorldProxy:test1()
local robot = Robot("Louie",world)
robot:look()
--robot.knowledge:dump()
_:expect(robot:factAt(3,0)).is("fact")
_:expect(robot:factAt(3,8),"exists not yet visible").is(nil)
robot:forward(1)
_:expect(robot:factAt(3,-1)).is("fact")
_:expect(robot:factAt(3,7)).is(nil)
robot:look()
_:expect(robot:factAt(3,7), "after second look").is("not visible on first look")
end)
Right. We need not to walk the line beyond the visibility distance.
function World:walkLine(lens, vec)
local packets = {}
local cells = Bresenham:drawLine(0,0, vec.x, vec.y)
table.remove(cells,1) -- take out the 0,0
for _i,v in ipairs(cells) do
local packet = self:newLookPacketAt(lens, v.x, v.y)
table.insert(packets, packet)
if packet._type == "OBSTACLE" then break end
end
return packets
end
We can check here for length. I’m not sure it’s the best place but I’m confident it’ll make the test work.
function World:walkLine(lens, vec)
local packets = {}
local cells = Bresenham:drawLine(0,0, vec.x, vec.y)
table.remove(cells,1) -- take out the 0,0
for _i,v in ipairs(cells) do
if v:len() > self.constants.visibility then break end
local packet = self:newLookPacketAt(lens, v.x, v.y)
table.insert(packets, packet)
if packet._type == "OBSTACLE" then break end
end
return packets
end
Test. All green except this:
28: walk all lines --
Actual: 412,
Expected: 512
We’ve reduced the number of cells we scan. I figure this is an approval test and it’s probably just fine.
I wander in the game, doing a few looks, and I get this final shadow area:
I think we’re good to go but the code needs some improvement first.
Let’s rename some variables:
function World:walkAllLines(lens)
local allPackets = {}
local targets = Bresenham:targets(self.constants.visibility)
for _i,target in ipairs(targets) do
local linePackets = self:walkLine(lens, target)
for _j,p in ipairs(linePackets) do
table.insert(allPackets, p)
end
end
return allPackets
end
This seems better, but reading it we realize that we are creating a list of LookPackets for each line and then copying that list into allPackets
. It would be better to accumulate them into allPackets
, passing that table down to walkLine
. I think we’ll defer that for now.
Test to be sure I haven’t messed up the renaming. Green. More tidying. This is pretty good but it shows one issue:
function World:walkLine(lens, vec)
local packets = {}
local cells = Bresenham:drawLine(0,0, vec.x, vec.y)
table.remove(cells,1) -- take out the 0,0
for _i,v in ipairs(cells) do
if v:len() > self.constants.visibility then break end
local packet = self:newLookPacketAt(lens, v.x, v.y)
table.insert(packets, packet)
if packet._type == "OBSTACLE" then break end
end
return packets
end
We’re removing the 0,0 element from each line. The result is that we don’t scan our own location, and we show it in shadow. I think I will deem that to be OK as well. Aside from that, I think the code is OK, but maybe this would be better:
function World:walkLine(lens, vec)
local packets = {}
local targetVectors = Bresenham:drawLine(0,0, vec.x, vec.y)
table.remove(targetVectors,1) -- take out the 0,0
for _i,v in ipairs(targetVectors) do
if v:len() > self.constants.visibility then break end
local packet = self:newLookPacketAt(lens, v.x, v.y)
table.insert(packets, packet)
if packet._type == "OBSTACLE" then break end
end
return packets
end
Yes, I prefer targetVectors
, especially since we aren’t using vectors very often in this game. They’re convenient here, and no one has to know.
Finally:
function World:newLookPacketAt(lens,x,y)
local fact = lens:factAt(x,y) or "LOOK"
return LookPacket:fromXY(fact,x,y)
end
I think that’s just fine. If there is no fact at x,y, we create a LOOK fact and packet.
Test. We’re green. Commit: world does not look past OBSTACLES, resulting in shadows in the radar as desired.
There’s a bit more to do: the old look
code is still in there. Delete these:
function World:look(robotName)
local robot = self._robots[robotName]
local packets = {}
if robot then
local lens = self._knowledge:newLensAt(robot._x, robot._y)
self:lookCircularIntoPackets(packets, lens)
end
return self:jsonLookResult(robot, packets)
end
function World:lookCircularIntoPackets(packets,lens)
local v = self.constants.visibility
for x = -v,v do
for y = -v,v do
if vec2(x,y):len() <= v then
self:lookFactInto(packets, lens, x,y)
end
end
end
end
function World:lookFactInto(packets, lens, x,y)
table.insert(packets, self:createPacketFor(lens,x,y))
end
Test. Green. Commit: remove old look methods in favor of new shadowing versions.
But wait, there’s more. We have this:
function Robot:look()
--self:markCellsLookedAt(self.worldinfo.visibility)
local callback = function(response) self:lookCallback(response) end
self._world:look(self._name,callback)
end
And the corresponding method markCellsLookedAt
. Delete the commented line and the method.
function Robot:markCellsLookedAt(v)
for x = -v,v do
for y = -v,v do
if vec2(x,y):len() <= v then
self:addFactAt("LOOK",x,y)
end
end
end
end
Test. Green. Commit: remove Robot:markCellsLookedAt and call to it. World now returns proper info.
One more thing. I’d like to make the game world more complicated now, just for fun. And maybe 8 is too far for visibility? Maybe I’ll rig up a parameter for that.
First let’s add some more stuff in the world setup.
function World:setUpGame()
local world = World(50,50)
-- left top right bottom
world:addObstacle(-5, 3, -5,-3)
world:addObstacle(-6, 2, -6,-2)
world:addObstacle(-9, 2, -7, 2)
world:addObstacle(-9,-2, -7,-2)
world:addObstacle(-9,-1, -9,-1)
world:addObstacle(-9, 1, -9, 1)
world:addPit( 3, 2, 3,-2)
world:addPit(-8, 1,-8, 1)
world:addPit(-8,-1,-8,-1)
world:addObstacle(4,0,6,0)
return world
end
This is a much more complicated world now. Here’s what I get if I go over by the left-side obstacles and look around.
Sweet. Commit: larger more complex initial world.
I noticed in setting up that little room on the left that the addObstacle
and addPit
require x and y to eavh be increasing, that is x1<x2 and y1<y2. We should relax that constraint, I think. But we’ve done more than enough for today, let’s sum up.
Summary
That went very smoothly. There was that one attempt at creating the lens higher up that didn’t work, but I just reverted and did it again and it did work. I have no real idea what my mistake was the first time. Possibly a local that went out of range.
I had thought that I’d have to spike the solution and then implement it with TDD, but once I thought of building it bottom up, I realized that I could TDD the pieces one at a time pretty easily. So up until the final installation, everything was driven nicely by tests.
Even the last bit was test driven, but in that less satisfactory way where old tests fail until the new code is up to snuff. With those it’s more of a bug hunt than a positive test asking for a new thing and new code creating it.
All in, though, I love the new radar. It’s almost too good, but we’ll see how it plays out as we go forward.
And we are unquestionably creating lots and lots of cells in knowledge. Let’s try something:
function Response:drawHUD()
local y = 80
local displayLine = function(line)
text(line,0,y)
y = y - 40
end
displayLine(self:result())
local pos = "("..self:x()..","..self:y()..")"
displayLine(pos)
displayLine(self:direction())
displayLine("Shots: "..self:shots())
displayLine("Shields: "..self:shields())
displayLine(self:message())
local ct = tostring(CurrentRobot.knowledge:factCount())
displayLine("Facts: "..ct)
end
I patched in a display of the number of facts in the Robot’s knowledge. Here’s what I got after driving around and looking around a bit:
We just might want to do something about that one of these days.
Still, this is good stuff. I wish people were getting the value that I’m getting out of it.
I’ll see the few of you next time!