There is a sticky note on my desk needing attention. Let’s attend to it. I have a small concern. Also: smaller steps pay off.

The note, as best as I can make out, says “tix frosbds fav multific faos, venive dups”. I’m pretty sure that means “Fix knowledge for multiple facts, remove dups”. Be that as it may, we do have an issue to deal with that sounds like that one.

The current storage for facts in the Knowledge class is a simple last in first out list: when a new fact comes in, we push it onto the front of a table,and when we look for facts and a given x,y, we search from the front of the table. So far that works find, but the size of that table grows without bound.

My initial plan, since a fact has x, y, and a string like “LOOK” or “OBSTACLE” or “ROBOT”, was to store facts in a dictionary indexed by x, containing a dictionary indexed by y, containing the string. That would actually work just fine … for now.

In fact, it works so closely to correctly that I think we’ll do it.

Knowledge

The germane bits of Knowledge are these:

Knowledge = class()

function Knowledge:init()
    self.facts = {}
end

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

function Knowledge:factCount()
    return #self.facts
end

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

Insert at the beginning, search from the beginning.

Let’s replace the Knowledge store, a simple table, with something better. And let’s TDD that something better as a separate class. Let’s call it XYStore.

XYStore

We write a trivial test:

        _:test("XYStore", function()
            local store = XYStore()
        end)

Which requires:

1: XYStore -- XYStore:17: attempt to call a nil value (global 'XYStore')

We parry that attack with:

XYStore = class()

The test attacks again:

        _:test("XYStore", function()
            local store = XYStore()
            store:save(11,31,"something")
            local what = store:fetch(11,31)
            _:expect(what).is("something")
        end)

In the spirit of the simplest thing, I respond with:

function XYStore:save(x,y,s)
    self._s = s
end

function XYStore:fetch(x,y)
    return self._s
end

Take that! I cry. The test desires to force me to do some work, and it attacks again:

        _:test("XYStore", function()
            local store = XYStore()
            store:save(11,31,"something")
            store:save(23,45,"other")
            local what = store:fetch(11,31)
            _:expect(what).is("something")
        end)

OK, enough fooling around, let’s do the thing.

function XYStore:init()
    self._xxx = {}
end

function XYStore:save(x,y,s)
    local xDict = self._xxx[x] or {}
    xDict[y]=s
    self._xxx[x] = xDict
end

function XYStore:fetch(x,y)
    local xDict = self._xxx[x] or {}
    return xDict[y]
end

I feel confident that the test is vanquished, but in a vain attempt to find me out, it attacks one more time:

        _:test("XYStore", function()
            local store = XYStore()
            store:save(11,31,"something")
            store:save(23,45,"other")
            local what = store:fetch(11,31)
            _:expect(what).is("something")
            _:expect(store:fetch(23,45)).is("other")
            _:expect(store:fetch(666,666)).is(nil)
        end)

My code resists the attack. I think XYStore is done.

But then the test attacks from an unexpected direction!

        _:test("XYStore", function()
            local store = XYStore()
            store:save(11,31,"something")
            store:save(23,45,"other")
            local what = store:fetch(11,31)
            _:expect(what).is("something")
            _:expect(store:fetch(23,45)).is("other")
            _:expect(store:fetch(666,666)).is(nil)
            _:expect(store:itemCount()).is(2)
        end)

A hit, a palpable hit! But I rally and respond:

function XYStore:itemCount()
    local count = 0
    for _k,xD in pairs(self._xxx) do
        for _l,s in pairs(xD) do
            count = count + 1
        end
    end
    return count
end

I feel that I am surely victorious. But the test tries one more time:

        _:test("count empty", function()
            local store = XYStore()
            _:expect(store:itemCount()).is(0)
        end)

I run the test with my left hand, showing contempt for the test. I am victorious at last.

Now let’s plug XYStore into Knowledge. But first, commit: XYStore saves single items under x and y keys.

Knowledge Upgrade

We surely have enough tests involving knowledge to plug this in without new ones.

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

We’ll continue to save the fact instance. We’re here to reproduce function, not to change it.

function Knowledge:addFactAt(content, x,y)
    self.facts:store(x,y,Fact(x,y,content))
end

And:

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

This becomes:

function Knowledge:factAt(x,y)
    local fact = self.facts:fetch(x,y) or Fact(x,y,nil)
    return fact.content
end

And:

function Knowledge:factCount()
    return #self.facts
end

This becomes:

function Knowledge:factCount()
    return self.facts:itemCount()
end

This gets removed:

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

This has no callers and gets removed:

function Knowledge:dump()
    for i,f in ipairs(self.facts) do
        print(f)
    end
end

We test. Everything breaks. We have said ‘store’ here, and the XYStore uses save. This is a hint that store is the better term and we change XYStore and its tests to match.

We are green. The game plays correctly, and facts grow rapidly but not insanely. Remember that in a radius 8 circle there are almost 200 cells to inspect, and since we include a “LOOK” item for each empty cell, we’ll have a storage memory of almost 200 items per scan. That’s better than thousands, but it’s still enough to make us wonder a bit.

Anyway, we’ve done what we planned, so commit: Knowledge uses XYStore, reducing storage requirements substantially.

I mentioned a concern in the article blurb.

Concern

The game supposedly includes other Robots in addition to our own. Those Robots are supposed to show up on our radar … and it is possible that that Robot would be “on top of” something else in the world. It could be in a Pit, for example.

The way things stand now, only the Pit or the Robot could appear in our Knowledge. On the Robot side, that might not even be noticed … but on the World side, if a Robot fell into a Pit (and if there was a way out of a Pit, perhaps after a delay), it’s likely that the Pit would be erased by the Robot, and then when the Robot moved … no more pit.

My current plan is to burn that bridge when we come to it. We are storing things inside Fact objects, which could be enhanced to allow for Robot plus something else, or some such thing. So I conclude “we’re not in big trouble, guys!”

Let’s sum up. We’ve actually seen a little lesson or two here.

Summary

There’s no question that we could have plugged a dictionary directly into Knowledge, and used it the same as we do in XYStore, and made Knowledge work in the new only one thing at a place fashion. But had we done so, while the tests we have would have served to get the job done, I think the job would have been a bit harder … and TDDing the separate XYStore was straightforward and simpler, and plugging it in was simple as well.

We changed a somewhat large step, plug a more complex store into Knowledge into two smaller steps, create a more complex store, and plug the existing new store into Knowledge. The two smaller steps went quite smoothly, and the second step fed back to the first, with “store is a better word than save”.

Smaller steps. Absolutely always better? Perhaps not, but that’s the way to bet.

Soon, I think I’ll be doing something completely different. And I am still open to ideas from all three of my readers.

See you next time!