I think we need to improve the map display a bit. We’ll spike, and that’s going to prove a point that I made yesterday, I hope. Or disprove, in which case we can laugh at me hahaha.

In testing the game on screen, I’ve noticed something that seems important. The “radar” is inactive. That is, you have to do a “look” command, and when you do, you get a scan of the column and row you’re in (and nothing else). The radar remembers what it has seen, so that you can build up a picture of the world. (Of course, if there are other robots, they can move, so that there are at least some things that could come into view without your knowing.)

I’ve noticed, in moving around, that I forget to look, with the result that there could be something in front of me that I’m not aware of. My plan is to change the display so that a cell that has been scanned and seen empty looks different from a cell that has never been scanned.

two scanned rows

In the picture above, I’ve scanned the two rows that show obstacles and pits … but I have also scanned the column that the robot is in, but there’s no indication of that. My plan is simple:

Change the display such that cells that have been looked at but are empty have a lighter display from cells that have never been scanned.

The question is … how should we do this?

Quick Design Session

One of the things that I find valuable is to have a “quick design session” before starting some story. This is particularly useful when pairing or working in ensemble, so that everyone is on the same page. But it’s also useful whenever a solo programmer sees multiple ways to do the story, or sees no good ways.

In short, think.

Among my thinks, I think I’d like to review how the display of a cell works now.

function drawCell(x,y, size)
    local fact = CurrentRobot:factAt(x,y)
    local gd = GridDisplay:display(fact)
    if gd.color then
        fill(gd.color)
    else
        noFill()
    end
    rect(x*size,y*size,size,size)
    if gd.text then
        fill(0,256,0)
        text(gd.text, x*size,y*size)
    end
    if gd.asset then
        pushMatrix()
        scale(0.8, 0.8)
        sprite(gd.asset,x,y)
        popMatrix()
    end
end

We get the fact at that location—no provision for more than one—and create a GridDisplay for it, which we then interpret in various ways to display the thing. (This is not brilliant code. Why aren’t we asking the GridDisplay to display itself? I’m glad we had this little think.) Anyway, what does GridDisplay do?

Reading GridDisplay answers some questions: GridDisplay isn’t a class providing instances. Instead it just returns little tables:

function GridDisplay:display(content)
    if content == "OBSTACLE" then
        return {color=color(0,256,0,128)}
    elseif content == "PIT" then
        return { color=color(0)}
    elseif content and content:sub(1,1) == "R" then
        local a = self:assetInDirection(content)
        return {asset=a, color = color(0,256,0,48)}
    else
        return {color=color(0,256,0,48)}
    end
end

Mostly, it returns a table specifying a color. And we notice that it has a default, which is good, because factAt can return nil:

function Knowledge:factAt(x,y)
    for i,fact in ipairs(self.facts) do
        if fact.x == x and fact.y == y then return fact.content end
    end
    return nil
end

We see that the “facts” are just triples with x, y, and content, and that if there is no fact at a given location, we return nil.

So … what if when we do a “look”, when we look at a cell, if it has no fact, we give it a new fact, indicating that it has been scanned?

Let’s look at look:

Here’s the code that creates the information LookPacket that we accumulate and return from look:

-- createPacketFor and directionTo
-- are in progress and need to support turns
-- seems like feature envy of some kind
function World:createPacketFor(lens,x,y)
    local content = lens:factAt(x,y)
    if not content then return nil end
    local dir = self:directionTo(x,y)
    local distance = math.max(math.abs(x), math.abs(y))
    return LookPacket(dir, distance, content)
end

I include those comments because they are germane. I don’t usually comment much at all, but I do like to leave clues in the code as to where trouble lurks. We’ll not deal with those, but the comments certainly raise my hackles a bit: I’d best be careful.

Let’s try an experiment. Let’s add a new fact when there is no content …

No! We’re on the wrong track. We don’t want to look at World. Our problem is local to the robot.

Could we do something in the Robot’s look? Well, at first glance, no. Robot look sends the request to the World and gets back only packets where there are things. So we’ll do a look and get back, in the case of our sample world, something like

W,5,OBSTACLE
E,3,PIT

The LookPacket class converts those to relative x and y coordinates, such as

{x=-5,y=0,OBSTACLE}
{x=3, y=0,PIT}

And we tuck those into our Knowledge, via our current Lens.

This Is More Interesting Than I Thought

Now it happens that we don’t know the order that things come back in. We don’t even know whether the look command will or won’t return information on the far side of obstacles or pits. (We know what we implemented, but the spec isn’t clear.)

We should probably be conservative, assuming that the scan stops at whatever thing is provided. So … I hypothesize that if we are given a packet like

x=-5,y=0,OBSTACLE

We can legitimately mark all the cells from -1 to -4 as scanned but we shouldn’t mark -6 and so on. What about rows or columns where we get no return ping? We don’t even know how large the world is. How far should our indication extend?

What if we take a different approach? What if, rather than mark cells, we were to display a line across a row we have scanned, and up and down any column?

Atypically, I’ve looked at the spec. When you launch a robot, you get back some important information. Values include visibility, reload time, repair time, mine-setting time, and maximum shield strength. (That last is unclear in the spec.) The important one for us is visibility, the maximum distance you can see when doing a look. So that’s useful … we can use that figure to limit how far we mark cells as seen.

I’m rather glad that I finally looked. I’m sure I must have read or scanned that fact, but I certainly didn’t remember it.

So … what if when we do a look, we were to begin by loading a “scanned” fact into the row and column we’re in, out to the limit of visibility? One problem with that is that as it’s written right now, you can’t have two facts in the same location. Well, you can, but the second one will never be found.

Let’s see what facts really look like. I don’t think we have a Fact class yet.

Well … we have and we haven’t:

function Knowledge:addFactAt(content, x,y)
    self:privateaddFactAt(Fact(x,y,content))
end

function Knowledge:factAt(x,y)
    for i,fact in ipairs(self.facts) do
        if fact.x == x and fact.y == y then return fact.content end
    end
    return nil
end

We’ve created and stored a Fact:

local Fact = class()

function Fact:init(x,y,content)
    self.x = x
    self.y = y
    self.content = content
end

But when we do factAt, we just return the content. Maybe that’s OK, we’ll find out.

A Plan Emerges

Here’s a sketch of a notion of an idea for a plan. What if, before we do a look, we create LOOK facts out to the visibility distance in all directions. Then we do the look, which will store some new facts, trying to overwrite the old ones. We’ll have a bug where the look stops working. We’ll fix it.

I’m going to spike that. I’m on a clean repo, so I’m safe to revert when this is over.

A Spike

I’ll start with the display, producing a new color.

function GridDisplay:display(content)
    if content == "OBSTACLE" then
        return {color=color(0,256,0,128)}
    elseif content == "PIT" then
        return { color=color(0)}
    elseif content and content:sub(1,1) == "R" then
        local a = self:assetInDirection(content)
        return {asset=a, color = color(0,256,0,48)}
    else
        return {color=color(0,256,0,48)}
    end
end

Let’s just add this:

function GridDisplay:display(content)
    if content == "OBSTACLE" then
        return {color=color(0,256,0,128)}
    elseif content == "LOOK" then
        return {color=color(0,256,0, 172)}
    elseif content == "PIT" then
        return { color=color(0)}
    elseif content and content:sub(1,1) == "R" then
        local a = self:assetInDirection(content)
        return {asset=a, color = color(0,256,0,48)}
    else
        return {color=color(0,256,0,48)}
    end
end

So far so good. We can test and commit that if we want to, but let’s not, we might change the whole idea. It’ll be easy to do again if this works.

Now in the robot’s look:

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

Let’s mark visibility:

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

And implement that:

local visibility = 8
function Robot:markColumnVisibility()
    for y = -visibility,visibility do
        if y ~= 0 then
            self:addFactAt("LOOK", 0,y)
        end
    end
end

function Robot:markRowVisibility()
    for x = -visibility,visibility do
        if x ~= 0 then
            self:addFactAt("LOOK", x,0)
        end
    end
end

function Robot:markVisibility()
    self:markRowVisibility()
    self:markColumnVisibility()
end

This, surprisingly enough, works. After I do a look from the starting position, I get this picture:

look with visibility

We might want to tweak the color, but that’s doing what I expected. But of course what we’ve seen doesn’t show up, because we don’t handle multiple facts per cell. The base addition looks like this:

function Knowledge:addFactAt(content, x,y)
    self:privateaddFactAt(Fact(x,y,content))
end

function Knowledge:privateaddFactAt(aFact)
    table.insert(self.facts, aFact)
end

What if we were to insert new facts at the beginning of the table instead of the end? That would make recent facts override old facts. Sounds about right.

function Knowledge:privateaddFactAt(aFact)
    table.insert(self.facts, 1, aFact)
end

That works a treat. Here’s the same look again:

visibility and things in the scan

I like this. When I move, the cell under me isn’t marked. We should let it get marked, just once:

function Robot:markRowVisibility()
    for x = -visibility,visibility do
        self:addFactAt("LOOK", x,0)
    end
end

I left the check for zero in markColumnVisibility, just to spare me the double marks. This is just a spike, why am I being so careful?

Let me work on the color. I think I’d like the color to be lighter than the background but not that bright. I think I want it between background and obstacle.

This

function GridDisplay:display(content)
    if content == "OBSTACLE" then
        return {color=color(0,256,0,128)}
    elseif content == "LOOK" then
        return {color=color(0,256,0, 96)}
    elseif content == "PIT" then
        return { color=color(0)}
    elseif content and content:sub(1,1) == "R" then
        local a = self:assetInDirection(content)
        return {asset=a, color = color(0,256,0,48)}
    else
        return {color=color(0,256,0,48)}
    end
end

looks about right:

improved colors

Spike complete. Let’s reflect.

Reflection

The first thing with a spike like this, for me, is that I want to keep it. The “rule” is that when we do a spike, we always throw it away. The good news here is that we have some broken tests that are seeing LOOK facts and previously expected nil facts. So unless I want to chase those, I’ve got to toss it.

I gather my strength and do the revert right now. There, we’re safe.

What have we learned?

  1. Color green transparency 96 looks pretty good;
  2. Using a Fact will work but we have to deal with duplicate facts somehow. (There is an ignored test about this, by the way, in case you’ve forgotten.)
  3. We need to fetch visibility from World.
  4. World needs to provide visibility.
  5. We should probably default visibility in case no one provides it.
  6. Don’t trust Ron to do everything perfectly.

As I reflect—that’s why we’re here after all—I wonder whether it would be better to have the visibility in a separate table. Or perhaps as a separate flag in a Fact.

I think I’d avoid separate tables, because of the need to maintain the Lens-Knowledge relationship. It would be easy enough but I see no big advantage to duplicating the tables.

We should probably be taking more advantage of the Fact object. Currently we just return its content but we could certainly extend Knowledge and Lens to have a few different fetching and storing methods. Fact could include both content and visibility, separately, for example. Of course, existence of a fact implies visibility, doesn’t it, since we only learn facts from doing the look.

The trick of pushing new facts to the top of the list is clever, but not smart. We should decide what to do about more than one fact per cell and implement it directly rather than by luck or implicit behavior.

Keeping my promise

What we have seen, however, is what I alluded to in the blurb: the fact that the implementation of Knowledge keeps its storage structure to itself means that we can change how it works with impunity. We reversed the order of its table and no one noticed. (The tests that failed were not due to that but to the “fact” that we store new facts that the tests didn’t anticipate.)

This is one reason why it’s generally a good idea to cover even simple collections with objects: we will surely find that they don’t quite want to be simple.

Summary

We’ll call it a morning with this spike, which has had very happy results, and soon, probably next time, we’ll implement something at least this good, with this experience fresh in our “mind”.

See you then!