If in fact I’m done, for a while at least, with the dungeon program, what shall I do and write about. Your input welcome.

I don’t promise to work on what you suggest. It has to catch my interest and needs to be something that I think Codea Lua would be suitable for. Mostly, it has to catch my interest.

Later today, I’l be talking with Colin Hammond, ScopeMaster, so it’s likely that I’ll have something to write about tomorrow morning. But what about today? I could take the morning off, but I hate to break my rhythm. It could throw the entire Earth off its axis.

Back when there had been shoemakers in living memory, there used to be a saying about the shoemaker’s children never having good shoes. The rule applies to programmers as well, in that often, our own tool set isn’t as helpful as it could be, because we’re always too busy making features or fixing defects to work on our tools. And I do recall that I’ve found some tests hard to write, and maybe it’s a good idea to look at my own very minimal toolset here on the iPad.

I think I’ll start with CodeaUnit but you may recall that I’ve built some first-stage program analysis tools as well. Maybe I’ll do a series on that sort of thing. But the evil of the day is sufficient thereto, so today we’ll just browse our tests and see if we get ideas for things CodeaUnit could help with.

Nota Bene: This is a mostly boring excursion through browsing code and finding nothing of interest. I am keeping it as a matter of record. I do not recommend reading further.

I’ll start us off with some things I can think of:

Possible Improvements to CodeaUnit

  • Additional checks beyond is, isnt, has, hasnt. Maybe islt or isbetween or even is_a to check class.
  • Some formatting improvements aimed at pasting results into my articles.
  • A bit more clarity on switches that control its output.
  • There may be enhancements in the Dungeon Main tab that should be moved to the CodeaUnit project base files.

Now I’ll browse the tests in the dungeon program to see if I notice anything.

Here’s a sequence of a lot of tests on a single result:

            _:expect(fl.yOffsetStart).is(50)
            _:expect(fl.lineSize).is(25)
            _:expect(fl.lineCount).is(4)
            _:expect(fl:linesToDisplay()).is(1)
            _:expect(fl:yOffset()).is(50)

There are two issues with this, neither one immense. One is that CodeaUnit is only smart enough to say something like Actual: 26 Expected: 25, so we have no real clue what went wrong. I’ve added a feature to CodeaUnit that allows a comment in the expect, like this:

            _:expect(fl.lineSize, "lineSize").is(25)

An even smaller notion is that we might have had a more compact notation, perhaps something like:

local v = {yOffsetStart=50,lineSize=25,lineCount=4 ... }
_:expect(fl).hasValues(v)

And hasValues would go through all the keys in v and access the values in fl and compare them. That would give us a slightly shorter form, and more clear output, since the hasValues would know all the field names.

My assessment is that this situation is rare and probably not worth it. But it does seem that we could even say

_:expect(fl).hasValues({lineSize=50})

And that is pretty clear and would provide the name in the output string.

I’ll rate this one “might be fun”. Let’s browse on …

        _:test("Create the monsters", function()
            local monsters = Monsters()
            local raw = monsters:rawTable()
            _:expect(#raw).is(0)
            local runner = FakeRunner()
            monsters:createRandomMonsters(runner,6,1)
            raw = monsters:rawTable()
            _:expect(#raw).is(6)
        end)

We do check table size frequently. It never says what it means in the output. Actual 5 Expected 6 sort of thing. We could do something like

_:expect(raw).hasSize(6)

One nice thing about CodeaUnit is that it is small and simple. Changes like this would make it larger and less simple. We’ll keep that in mind.

Browsing on …

Here’s a thing that has come up a few times, checking all the entries in a table. This one is quite complex, and perhaps not one to cater to directly:

        _:test("MonsterEntry table quality", function()
            local MT = Monster:initMonsterTable()
            for i,mte in ipairs(MT) do
                checkMTentry(i,mte)
            end
        end)

function checkMTentry(num, mte)
    local temp
    local name = mte.name
    _:expect(name,"monster "..num.." has no name").isnt(nil)
    checkIsTable(mte,name,"health", 2)
    checkIsTable(mte,name,"strength", 2)
    checkIsTable(mte,name,"speed", 2)
    checkIsTable(mte,name,"moving")
    checkIsTable(mte,name,"hit")
    checkIsTable(mte,name,"dead")
end

function checkIsTable(mte, name, tabName, length)
    local t = mte[tabName]
    _:expect(type(t), name.." "..tabName).is("table")
    if length then
        _:expect(#t, name.." length "..tabName).is(length)
    end
end

Conceivably we could write something like

_:expect(MT).checkEach(...)

But I don’t see much that we could do to improve things.

Browsing right along … here’s one that bugs me frequently:

        _:test("Horizontal corridor", function()
            local tile
            local msg
            local dungeon = runner:getDungeon()
            local room = Room(1,1,1,1, dungeon:asTileFinder())
            room:horizontalCorridor(5,10, 7)
            tile = runner:getTile(vec2(4,7))
            _:expect(tile:isEdge()).is(true)
            tile = runner:getTile(vec2(11,7))
            _:expect(tile:isEdge()).is(true)
            r,x,y = checkRange(dungeon, 5,7, 10,7, Tile.isFloor)
            msg = string.format("%d %d", x, y)
            _:expect(r,msg).is(true)
        end)

As much as I hate the output message “Actual: 5, Expected: 6”, I hate “Actual: false, Expected: true” even more. Note that in the final expect there, I actually formatted a careful message to improve the output a bit.

This is a reach, but what about:

_:expect(tile).respondsTo("isEdge").with(true)

That would allow CodeaUnit to print a better message, and I can imagine people who would argue that the expectation is more readable in that long form. I am not one of them, though I can appreciate the point.

Moving along, I find this:

            local r3x,r3y = r3:center()
            _:expect(r3x).is(3)
            _:expect(r3y).is(12)

I’m not sure whether that’s an example of something CodeaUnit could help with, or just a reason not to return multiple results very often.

Then there’s this:

            r2:hvCorridor(r1)
            checkRange(dungeon, r2x,r2y, r1x, r2y, Tile.isFloor)
            checkRange(dungeon, r1x,r2y, r1x, r1y, Tile.isFloor)

function checkRange(dungeon, x1, y1, x2, y2, checkFunction)
    x1,x2 = math.min(x1,x2), math.max(x1,x2)
    y1,y2 = math.min(y1,y2), math.max(y1,y2)
    for x = x1,x2 do
        for y = y1,y2 do
            local t = dungeon:getTile(x,y)
            local r = checkFunction(t)
            if not r then
                local msg = string.format("checkRange %d,%d fails", x,y)
                _:expect(r,msg).is(true)
                return r,x,y
            end
        end
    end
    _:expect(true).is(true)
    return true,0,0
end

That’s rather specialized, it’s checking a hallway or something. I don’t see much hope for a feature to help with it.

Here’s an interesting one:

        _:test("monsters level one correctly chosen", function()
            local mt = Monster:getMonstersAtLevel(1)
            local MT = Monster:getMT()
            local c = 0
            for i,m in ipairs(MT) do
                if m.level == 1 then c = c + 1 end
            end
            _:expect(#mt).is(c)
            for i,m in ipairs(mt) do
                _:expect(m.level).is(1)
            end
        end)

Here we are trying to test that the number of monsters returned by getMonstersAtLevel(1) is in fact equal to the number available, and that all the monsters we got are in fact level 1.

I don’t think that CodeaUnit could help much with that, but I wonder if my functional programming operators could.

        _:test("monsters level one correctly chosen", function()
            local mt = Monster:getMonstersAtLevel(1)
            local MT = Array(Monster:getMT())
            local c = #MT:filter(function(m) return m.level == 1 end)
            _:expect(#mt).is(c)
            for i,m in ipairs(mt) do
                _:expect(m.level).is(1)
            end
        end)

Better? Maybe. I’ll revert it, I’m not here to improve the Dungeon program.

I’ve found a number of tests that do not express clearly to me what they’re testing. Let’s look at one.

        _:test("is open hallway tile left right", function()
            local tile, up, down, left, right
            tile = Tile:hall(100,100, runner:tileFinder())
            up = Tile:wall(100,101, runner:tileFinder())
            down = Tile:wall(100,99, runner:tileFinder())
            left = Tile:hall(99,100, runner:tileFinder())
            right = Tile:hall(101,100, runner:tileFinder())
            _:expect(tile:isOpenHallway(up,down,left,right)).is(true)
            up = Tile:hall(100,101, runner:tileFinder())
            down = Tile:hall(100,99, runner:tileFinder())
            _:expect(tile:isOpenHallway(up,down,left,right), "up down left right not OK").is(false)
        end)

Let’s see here. We’re creating a hallway tile named tile and above and below it we put walls, and to its left and right we put hallway.

Then we ask whether it is open hallway and we expect that it is, and then we put hallway tiles above and below it and expect isOpenHallway to answer false. That seems rather backward to me. The method, which of course mimics this behavior, is:

function Tile:isOpenHallway(up,down,left,right)
    if not self:isHall()  or not self:isEmpty() then return false end
    return up:isHall() and down:isHall() and not left:isHall() and not right:isHall()
    or left:isHall() and right:isHall() and not up:isHall() and not down:isHall()
end

I gotta see who’s sending this and why.

function DungeonBuilder:randomHallwayTile()
    while true do
        local pos = vec2(math.random(1, self:tileCountX()), math.random(1,self:tileCountY()))
        local tile = self:getTile(pos)
        local up = self:getTile(pos+vec2(0,1))
        local down = self:getTile(pos-vec2(0,1))
        local left = self:getTile(pos-vec2(1,0))
        local right = self:getTile(pos+vec2(1,0))
        if tile:isOpenHallway(up,down,left,right) then return tile end
    end
end

OK, fine, who does that?

function DungeonBuilder:placeSpikes(count)
    for i = 1,count or 20 do
        local tile = self:randomHallwayTile()
        Spikes(tile)
    end
end

What this seems to me to be doing is that isOpenHallway actually returns true if the tile in question is a hall tile and has hall tiles either on both sides of it it or above and below it, but not both.

I think that’s a poor name, should be isNarrowHallway or something. I’m not going to change it just now, I don’t consider the Dungeon program to be open, I’m just browsing it.

That note aside, I don’t see much of anything that CodeaUnit could help us with there.

Browsing further, I see an example of has and hasnt:

        _:test("dcPrepare removes old dungeon contents", function()
            local runner = GameRunner()
            local builder = runner:defineDungeonBuilder()
            builder:dcPrepare()
            local object = "object"
            local tile = "tile"
            Bus:publish("moveObjectToTile", object, tile)
            local contents = Bus:query("getTileContents", tile)
            _:expect(contents).has(object)
            builder:dcPrepare()
            contents = Bus:query("getTileContents", tile)
            _:expect(contents, "it's still there!").hasnt(object)
        end)

My recollection is that the diagnostic from those messages is weak. Let me reverse those and print the results for us to look at:

2: dcPrepare removes old dungeon contents  -- Actual: table: 0x28c8a9f80, Expected: object
2: dcPrepare removes old dungeon contents it's still there! -- Actual: table: 0x28c9fa540, Expected: object

I suppose that’s not terrible. I think we could have a better message, something that mentions that it’s a table.hasnt or something. I’ll make a note of that.

The test for Button is wild! Let me include it here just for our mutual amazement:

function testButton()
    CodeaUnit.detailed = false
    
    local _runner
    local _handler
    local _captureCount
    local _player
    
    _:describe("Button", function()
        
        _:before(function()
            _bus = Bus
            Bus = {subscribe=function() end}
            _captureCount = 0
            _hander = nil
            _fooCalled = false
            _runner = Runner
            _player = {itsOurTurn=function(_self)
                    return true
                end,
                foo=function() _fooCalled = true end,
                turnComplete=function() end
            }
            Runner={capture=function(_self, aFunction)
                    if _captureCount%2 ==0 then
                        _handler = aFunction
                        _:expect(type(_handler), "first time").is("function")
                    else
                        handler = nil
                        _:expect(aFunction, "second time").is(nil)
                    end
                    _captureCount = _captureCount + 1
                end   
            }
        end)
        
        _:after(function()
            Bus = _bus
            Runner = _runner
        end)
        
        _:test("Callback Test", function()
            local callbackButton = nil
            local b = Button("foo", 100,100, 20,20)
            local t = {pos=vec2(105,105), state=BEGAN}
            local tEnd = {pos=vec2(200,200), state=ENDED}
            _:expect(b:isPosMine(t.pos)).is(true)
            b:touchBegan(t.pos, function(button)
                callbackButton = button
            end)
            _:expect(callbackButton).is(b)
        end)
        
        _:test("Touch End", function()
            local b = Button("foo", 100,100, 20,20)
            local t = {pos=vec2(105,105), state=BEGAN}
            _fooCalled = false
            _:expect(b:isPosMine(t.pos)).is(true)
            b:touchEnded(vec2(200,200), _player)
            _:expect(_fooCalled).is(false)
            b:touchEnded(vec2(104,106), _player)
            _:expect(_fooCalled).is(true)
        end)
        
    end)
end

I hope I wrote a really good article about that. I’m not even going to try to explain it now, clearly we are emulating the behavior we expect from the player and runner when buttons are touched. I doubt that CodeaUnit could have helped much with that.

I find in the tests for CombatRound that I created a couple of fake objects and even an array of fake random numbers, which are used if available, which they are only in my fake testing objects. I’ll spare you reading that.

I note in some of the tests I’ve browsed that there’s extra work done because I sometimes use tween.delay to cause things to happen asynchronously. For example, every so often, the MusicPlayer looks to see if the player is close to a monster and if so, plays more dramatic music. That complicates the tests because during testing I don’t want the delays.

I think that code could use a little improvement, but that’s not our purpose here.

I run across the tab “Test Frame”, which includes a starting test layout. I’d like to be able to include that in my CodeaUnit project base. Make a note.

The GlobalAnalyzer in Dungeon is a generally useful tool and should probably be included in the CUBase.

The finite state machine, FSM, used in the NPC, is worth at least remembering, in case we need it again. It’s probably not yet robust enough to be made into a library, and anyway we don’t need it.

The class ActionSequence is interesting. It’s just used by the Chest, which, upon each bump from the player, steps through the sequence of opening, rezzing the loot, doing damage, and finally becoming able to be stepped upon and passed over. ActionSequence is sort of a tiny FSM. It was invented late on in the game but might have been useful for earlier purposes, as, I suppose, might FSM.

I’ve reached the end of the tests. Here, including the starting ideas, are all my notes:

Ideas

  • Additional checks beyond is, isnt, has, hasnt. Maybe islt or isbetween or even is_a to check class.
  • Some formatting improvements aimed at pasting results into my articles.
  • A bit more clarity on switches that control its output.
  • There may be enhancements in the Dungeon Main tab that should be moved to the CodeaUnit project base files.

  • _:expect(pos).hasValues{x=5,y=7}
  • _:expect(raw).hasSize(6)
  • _:expect(tab).checkEach(?)
  • improve message from has and hasn’t?
  • Include empty starting test frame tab in CUBase?
  • Include GlobalAnalyzer in CUBase?
  • FSM, Tale, and ActionSequence may be worth working on someday.