I really want to make some progress on the creatures that hang around the WayDown. But first, I am going to write a Coda on CodeaUnit and my simple toolset.

OK, it’s 0925 and the Coda is done. I still have part of a brain and part of a granola bar and part of a chai left, so let’s see what we can do about the game.

I wrote a new strategy, HangoutMonsterStrategy, with the rule being that it knows a tile, and when it’s far from the tile it moves toward it and when it’s near it moves away, moving randomly otherwise.

HangoutMonsterStrategy = class()

function HangoutMonsterStrategy:init(monster, tile)
    self.monster = monster
    self.tile = tile
    self.min = 3
    self.max = 6
end

function HangoutMonsterStrategy:execute(dungeon)
    local method = self:selectMove(self.monster:manhattanDistanceFromTile(tile))
    self.monster[method](self.monster, dungeon, self.tile)
end

function HangoutMonsterStrategy:selectMove(range)
    if range < self.min  then
        return "basicMoveAwayFromTile"
    elseif self.min <= range  and range <= self.max then
        return "basicMoveRandomly"
    else
        return "basicMoveTowardTile"
    end
end

I tried the other day to create a Monster who would do that, but I got into trouble. We’ll try again today.

Let’s start where Monsters are created. Part of my problem before was that I had fixated on the GameRunner doing it.

Monsters class has this:

function Monsters:createRandomMonsters(runner, count, level)
    self.table = Monster:getRandomMonsters(runner, count, level)
end

And Monster class has this:

function Monster:getRandomMonsters(runner, number, level)
    local t = self:getMonstersAtLevel(level)
    local result = {}
    for i = 1,number do
        local mtEntry = t[math.random(#t)]
        local tile = runner:randomRoomTile(1)
        local monster = Monster(tile, runner, mtEntry)
        table.insert(result, monster)
    end
    return result
end

The Monsters class has responsibility for moving all the monsters, so that we need to ensure that it adds our new Hangout monster to its collection. But let’s give it another method for the hangout monster creation:

function Monsters:createHangoutMonsters(runner, count, tile)
    local tab = Monster:getHangoutMonsters(runner, count, tile)
    for i,m in ipairs(tab) do
        table.insert(self.table, m)
    end
end

I see that I’m not TDDing this. I don’t want to–I really don’t want to–this area is messy and I don’t like to test near it. This is a very bad sign.

Here’s my attempt at the get:

function Monster:getHangoutMonsters(runner, number, tile)
    local result = {}
    local mtEntry = {name="Poison Frog", level=3, health={4,8}, speed = {2,6}, strength={8,11},
    attackVerbs={"leaps at", "smears poison on", "poisons", "bites"},
    dead=asset.frog_dead, hit=asset.frog_hit,
    moving={asset.frog, asset.frog_leap}}
    for i = 1,number do
        local monster = Monster(tile, runner, mtEntry)
        monster:setMovementStrategy(HangoutMonsterStrategy(monster,tile))
        table.insert(result,monster)
    end
    return result
end

I’ve just copied the poison frog table entry, and I’ve posited a method on Monster to set the strategy. I’ll provide that:

function Monster:setMovementStrategy(strategy)
    self._movementStrategy = strategy
end

Now let’s create some of these guys. That will get done in GameRunner:

function GameRunner:setupMonsters(n)
    self.monsters = Monsters()
    self.monsters:createRandomMonsters(self, 6, self.dungeonLevel)
    self.monsters.createHangoutMonsters(self,2, self.wayDown.tile)
end

I think there is a slim but real chance that this will work.

I get this error:

Monster:25: bad 'for' limit (number expected, got table)
stack traceback:
	Monster:25: in method 'getHangoutMonsters'
	Monsters:53: in field 'createHangoutMonsters'
	GameRunner:369: in method 'setupMonsters'
	GameRunner:102: in method 'createLevel'
	Dungeon:15: in field '_before'
	CodeaUnit:44: in method 'test'
	Dungeon:26: in local 'allTests'
	CodeaUnit:16: in method 'describe'
	Dungeon:9: in function 'testDungeon'
	[string "testDungeon()"]:1: in main chunk
	CodeaUnit:139: in field 'execute'
	Tests:506: in function 'runCodeaUnitTests'
	Main:6: in function 'setup'

That’s here:

function Monster:getHangoutMonsters(runner, number, tile)
    local result = {}
    local mtEntry = {name="Poison Frog", level=3, health={4,8}, speed = {2,6}, strength={8,11},
    attackVerbs={"leaps at", "smears poison on", "poisons", "bites"},
    dead=asset.frog_dead, hit=asset.frog_hit,
    moving={asset.frog, asset.frog_leap}}
    for i = 1,number do
        local monster = Monster(tile, runner, mtEntry)
        monster:setMovementStrategy(HangoutMonsterStrategy(monster,tile))
        table.insert(result,monster)
    end
    return result
end

Number is a table? Must have called it wrong from Monsters?

function Monsters:createHangoutMonsters(runner, count, tile)
    local tab = Monster:getHangoutMonsters(runner, count, tile)
    for i,m in ipairs(tab) do
        table.insert(self.table, m)
    end
end

OK, maybe GameRunner:

function GameRunner:setupMonsters(n)
    self.monsters = Monsters()
    self.monsters:createRandomMonsters(self, 6, self.dungeonLevel)
    self.monsters.createHangoutMonsters(self,2, self.wayDown.tile)
end

OK, that’s a two, it’s passed on down. Now I am confuzzled.

function Monsters:createHangoutMonsters(runner, count, tile)
    print("create ", count)
    local tab = Monster:getHangoutMonsters(runner, count, tile)
    for i,m in ipairs(tab) do
        table.insert(self.table, m)
    end
end

And that prints this:

create 	Tile[67][56]: room

It’s got the tile. What have I done wrong here? Ah: Standard error number 631 or something: Dot should be colon:

function GameRunner:setupMonsters(n)
    self.monsters = Monsters()
    self.monsters:createRandomMonsters(self, 6, self.dungeonLevel)
    self.monsters:createHangoutMonsters(self,2, self.wayDown.tile)
end

Some tests fail, probably the ones that count monsters. Let’s see if we can find our frogs. As soon as I move, I get this:

Tile:328: attempt to index a nil value (local 'aTile')
stack traceback:
	Tile:328: in function <Tile:327>
	(...tail calls...)
	MonsterStrategy:66: in method 'execute'
	Monster:250: in method 'executeMoveStrategy'
	Monster:201: in method 'chooseMove'
	Monsters:69: in method 'move'
	GameRunner:339: in method 'playerTurnComplete'
	Player:237: in method 'turnComplete'
	Button:59: in method 'performCommand'
	Button:53: in method 'touched'
	GameRunner:379: in method 'touched'
	Main:39: in function 'touched'

This is the first use of basicMoveAwayFromTile and basicMoveTowardTile, since I have no damn tests for them. See what kind of trouble you get in when this happens? I’m forced to resort to debugging. And in Codea, that comes down to printing stuff.

I find this:

function HangoutMonsterStrategy:execute(dungeon)
    local method = self:selectMove(self.monster:manhattanDistanceFromTile(tile))
    self.monster[method](self.monster, dungeon, self.tile)
end

Note that tile in the selectMove call. Nil. Should be self.tile.

A TDD test would have found this quickly. Why am I not doing one? Why am I sitting here hammering this instead of doing what I should?

Well, a) I am human, as far as you know. And b) I feel close to it working, and c) writing the test seems hard. It probably isn’t, but it feels that way.

The big fool presses on …

Tile:328: attempt to index a nil value (local 'aTile')
stack traceback:
	Tile:328: in function <Tile:327>
	(...tail calls...)
	Dungeon:111: in method 'availableTilesFurtherFromTile'
	Monster:169: in field '?'
	MonsterStrategy:67: in method 'execute'
	Monster:254: in method 'executeMoveStrategy'
	Monster:205: in method 'chooseMove'
	Monsters:69: in method 'move'
	GameRunner:339: in method 'playerTurnComplete'
	Player:237: in method 'turnComplete'
	Player:176: in method 'keyPress'
	GameRunner:289: in method 'keyPress'
	Main:33: in function 'keyboard'

I just let out a huge sigh. I am disappointed and the wind is nearly out of my sails. One of my prints gives good news:

away 	Tile[3][33]: room

So we have a tile going in from here:

function Monster:basicMoveAwayFromTile(dungeon, tile)
    print("away ", tile)
    local tiles = dungeon:availableTilesFurtherFromTile(tile)
    print("got ", #tiles)
    self.tile = self.tile:validateMoveTo(self,tiles[math.random(1,#tiles)])
end

And we didn’t print a “got”. So we look here:

function Dungeon:availableTilesFurtherFromTile(tile1, tile2)
    local desiredDistance = self:manhattanBetween(tile1,tile2) + 1
    return self:availableTilesAtDistanceFromTile(tile1,tile2, desiredDistance)
end

Small matter of needing two tiles, not one.

function Monster:basicMoveAwayFromTile(dungeon, tile)
    print("away ", tile)
    local tiles = dungeon:availableTilesFurtherFromTile(self.tile, tile)
    print("got ", #tiles)
    self.tile = self.tile:validateMoveTo(self,tiles[math.random(1,#tiles)])
end

function Monster:basicMoveTowardTile(dungeon, tile)
    print("toward ", tile)
    local tiles = dungeon:availableTilesCloserToTile(self.tile, tile)
    print("got ", #tiles)
    self.tile = self.tile:validateMoveTo(self,tiles[math.random(1,#tiles)])
end

Here again, I’m sure a test would have found this sooner and more easily. Still, my hopes are high …

I follow the hearts, which still appear if I press Flee, and find this going on near the WayDown:

frogs, one moving around, one stuck

Well now, it worked. However, it happens that the one frog in the corner is stuck. He wants to move away from the WayDown, but there are no cells to move to. So he is locked to his existing cell. We have an opportunity to improve the hangout strategy.

We also have the Hangout monsters working. Remove prints. Commit: Poison frogs hang around the WayDown.

Whew! That was a near thing, I was holding on mby my fingernails. It’s 1021, so coding that and making it work took almost an hour. I like to see things get done in 20 minutes or less, so this was pretty scary.

Tests would really have helped me find those parameter mismatches. But I don’t have them, and one I don’t have them, it’s really hard to get me to go back and write them.

There is a lesson there for me … and maybe even for you.

Now, I’ll quickly sum up and leave you to your day.

Summary

Starting in the right place, the Monsters class, made this go well. We have broken tests right now, because of the new monsters showing up. I’ve decided to let that slide for now, though I may go back and fix them later today, offline.

The defects in the new capabilities were rather simple: failure to type a self, failure in the calling sequence, and so on. They were semi-easy to find, but the difficulty was increased because I had to run the program to cause them, and couldn’t focus on a particular need as one could in a test: I had to deal with whatever the Dungeon served up.

Don’t do like my brother’s brother. Don’t work without tests if you can possibly avoid it. You’ll be glad you have them.

See you next time!


D2.zip


Coda: CodeaUnit etc

When I start a new Codea project, I generally intend to use TDD, unless it’s just a tiny thing to check how some Lua or Codea feature works. (And even then, I’d do well to TDD, and often do. The tests help make clear what I’m doing, as I’ll perhaps demonstrate below, if time permits.

I use a modified version of CodeaUnit as my base for development. CodeaUnit was provided by “jakesankey” on the Codea Talk forum, back in 2015. I grabbed a copy and have used it ever since, with some mods.

CUBase

I begin a project with a Codea template that I’ve created and saved. When you start a new project, Codea copies the template you provide, or its default one, to create the initial state of the code. My template is called CUBase, and it looks like this.

The Main tab:

-- CUBase

function setup()
    if CodeaUnit then 
        codeaTestsVisible(true)
        runCodeaUnitTests() 
    end
end

function draw()
    if CodeaUnit then showCodeaUnitTests() end
end


-- ------ functions below here

function runCodeaUnitTests()
    local det = CodeaUnit.detailed
    CodeaUnit.detailed = false
    Console = _.execute()
    CodeaUnit.detailed = det
end

function showCodeaUnitTests()
    if CodeaVisible then
        background(40, 40, 50)
        pushMatrix()
        pushStyle()
        fontSize(50)
        textAlign(CENTER)
        if not Console:find("0 Failed") then
            stroke(255,0,0)
            fill(255,0,0)
        elseif not Console:find("0 Ignored") then
            stroke(255,255,0)
            fill(255,255,0)
        else
            fill(0,255,0,50)
        end
        text(Console, WIDTH/2, HEIGHT-100)
        popStyle()
        popMatrix()
    end
end

function codeaTestsVisible(aBoolean)
    CodeaVisible = aBoolean
end

And the Tests tab:

-- RJ 20200911
-- Delete these and replace with your own.

function testYOURTESTNAMEHERE()
    CodeaUnit.detailed = true

    _:describe("YOUR_TEST_NAME_HERE", function()

        _:before(function()
        end)

        _:after(function()
        end)

        _:test("HOOKUP", function()
            _:expect("Foo", "testing fooness").is("Bar")
        end)
        
        _:test("Floating point epsilon", function()
            _:expect(1.45).is(1.5, 0.1)
        end)

    end)
end

The idea, as you can guess from the CAPS, is to give your test function a name relating to your project, and to fill in the other text as shown. There are just two tests in there to start me off.

The first one is the standard “hookup” test that I always like to do, which will fail until I fix it or remove it. The second passes, but it shows how I modified CodeaUnit to allow a floating point test’s is to include an epsilon, the amount by which the comparison is allowed to differ. Floating point equality is hard to get.

Note that in the HOOKUP test, there is a string in the expect, after the expected value. If a string is included there, it will be displayed in the output.

When you create the project and run it, it looks like this:

first run of project shows errors and test button

I’ve modified CodeaUnit to display a results summary at the top of the screen in bright red type. When you fix the bug in the hookup, by replacing “Foo” with “Bar” (or vice versa, your choice), the run looks like this:

run with good tests shows in green

At the left of the screen, in the Codea console, you’ll see the details of the test run. At the top left is a button you can press to run the tests again. I prefer to run them automatically, because I’m lazy.

Note: Sometimes, when you change the name of a test suite, or add a new one, CodeaUnit does not seem to pick it up. It can just miss out the suite entirely, or it commonly fails looking for the old name. I think this is due to Codea itself trying to save time by not saving files that it thinks don’t need it. Whatever it is, running again, or, worst case, holding down the “Run” arrow at top right of the editor and picking the Save and Run option, fixes the issue.

CodeaUnit

CodeaUnit is not included as part of CUBase. It is included as a Codea dependency. Codea dependencies refer from one project to another, and act as if all the tabs of the dependency were added to your project, except for the Main tab. So I have a separate project, CodeaUnit, which is referred to as a dependency by my CUBase template, and therefore by your new project that uses CUBase.

In the CodeaUnit project, the CodeaUnit tab contains all of CodeaUnit itself:

CodeaUnit = class()

function CodeaUnit:describe(feature, allTests)
    self.tests = 0
    self.ignored = 0
    self.failures = 0
    self._before = function()
    end
    self._after = function()
    end

    local f_string = string.format("Feature: %s", feature)
    CodeaUnit._summary = CodeaUnit._summary .. f_string .. "\n"
    print(f_string) 

    allTests()

    local passed = self.tests - self.failures - self.ignored
    local summary = string.format("%d Passed, %d Ignored, %d Failed", passed, self.ignored, self.failures)
    CodeaUnit._summary = CodeaUnit._summary .. summary .. "\n"
    print(summary)
end

function CodeaUnit:before(setup)
    self._before = setup
end

function CodeaUnit:after(teardown)
    self._after = teardown
end

function CodeaUnit:ignore(description, scenario)
    self.description = tostring(description or "")
    self.tests = self.tests + 1
    self.ignored = self.ignored + 1
    if CodeaUnit.detailed then
        print(string.format("%d: %s -- Ignored", self.tests, self.description))
    end
end

function CodeaUnit:test(description, scenario)
    self.description = tostring(description or "")
    self.tests = self.tests + 1
    self._before()
    local status, err = pcall(scenario)
    if err then
        self.failures = self.failures + 1
        print(string.format("%d: %s -- %s", self.tests, self.description, err))
    end
    self._after()
end

function CodeaUnit:expect(conditional, msg)
    local message = string.format("%d: %s %s", (self.tests or 1), self.description, (msg or ""))

    local passed = function()
        if CodeaUnit.detailed then
            print(string.format("%s -- OK", message))
        end
    end

    local failed = function()
        self.failures = self.failures + 1
        local actual = tostring(conditional)
        local expected = tostring(self.expected)
        print(string.format("%s -- Actual: %s, Expected: %s", message, actual, expected))
    end

    local notify = function(result)
        if result then
            passed()
        else
            failed()
        end
    end

    local is = function(expected, epsilon)
        self.expected = expected
        if epsilon then
            notify(expected - epsilon <= conditional and conditional <= expected + epsilon)
        else
            notify(conditional == expected)
        end
    end

    local isnt = function(expected)
        self.expected = expected
        notify(conditional ~= expected)
    end

    local has = function(expected)
        self.expected = expected
        local found = false
        for i,v in pairs(conditional) do
            if v == expected then
                found = true
            end
        end
        notify(found)
    end

    local hasnt = function(expected)
        self.expected = expected
        local missing = true
        for i,v in pairs(conditional) do
            if v == expected then
                missing = false
            end
        end
        notify(missing)
    end
    
    local throws = function(expected)
        self.expected = expected
        local status, error = pcall(conditional)
        if not error then
            conditional = "nothing thrown"
            notify(false)
        else
            notify(string.find(error, expected, 1, true))
        end
    end

    return {
        is = is,
        isnt = isnt,
        has = has,
        hasnt = hasnt,
        throws = throws
    }
end

CodeaUnit.execute = function()
    CodeaUnit._summary = ""
    for i,v in pairs(listProjectTabs()) do
        local source = readProjectTab(v)
        for match in string.gmatch(source, "function%s-(test.-%(%))") do
            print("loading", match)
            load(match)() -- loadstring pre Lua 5.4
        end
    end
    return CodeaUnit._summary
end

CodeaUnit.detailed = true



_ = CodeaUnit()

parameter.action("CodeaUnit Runner", function()
    CodeaUnit.execute()
end)

-- make these codeaunit.etc
-- create a fake main?
-- create a starting tests tab

-- or do we start from an example?

function runTests()
    local det = CodeaUnit.detailed
    CodeaUnit.detailed = false
    Console = _.execute()
    CodeaUnit.detailed = det
end

function showTests()
    pushMatrix()
    pushStyle()
    fontSize(50)
    textAlign(CENTER)
    if not Console:find("0 Failed") then
        stroke(255,0,0)
        fill(255,0,0)
    elseif not Console:find("0 Ignored") then
        stroke(255,255,0)
        fill(255,255,0)
    else
        fill(0,128,0)
    end
    text(Console, WIDTH/2, HEIGHT-200)
    popStyle()
    popMatrix()
end

As I look at this, I note the functions runTests and showTests in there. Because of the way I’ve set up my template, I have my own versions of those, called runCodeaUnitTests and showCodeaUnitTests, so that when I tweak things in a given project, I’m not breaking CodeaUnit. If you start using CodeaUnit, you can use the functions shown here, or mine, either way. If you have questions, you can contact me while I’m still alive on the Codea Talk forum.

The Main tab, in CodeaUnit is essentially empty and of course it’s not used in your project: your own Main is.

A Little Demo

Let’s do a little demo of how to use this. What shall we create? Let’s do some of the table functions from yesterday, select/filter and map. Maybe more.

I create a project, Table, template CUBase. I fix the top line of Main:

-- CUBase

Becomes:

-- Table operations

In the Tests tab, I rename things, and remove the floating test:

-- RJ 20210319
-- select and map, maybe more

function testTableOperations()
    CodeaUnit.detailed = true

    _:describe("Table Operations", function()

        _:before(function()
        end)

        _:after(function()
        end)

        _:test("HOOKUP", function()
            _:expect("Foo", "testing fooness").is("Bar")
        end)

    end)
end

Note that I leave the hookup and I leave it broken. I run the tests now for the first time. Red, as expected:

first run of table tests is red

Now, on a paranoid day, I’ll fix the hookup test and run again. On a less paranoid day, I’m convinced that I’m hooked up and I’ll replace the hookup with my first real test, or add a new one, depending how I feel about things.

I know I’m going to test a number of things, but with an initial test I usually stop after the first assert:

        _:test("map table", function()
            local tab = {1,2,3,4}
            local mapped = map(tab, function(each) return 2*each end)
            _:expect(#tab).is(4)
        end)

This is enough to make me write the map function. (I’m just going to create global functions for now. In the Dung program itself, I may one day go further.)

I could use “fake it till you make it” by just returning the input:

function map(tab,f)
    return tab
end

Lately, I’ve begun keeping the real code and the tests in the same tab. That reduces the number of tabs I have, which is getting to be an issue.

Anyway, that should make the test pass, and it does.

Of course we really want to loop over the table, producing a new one:

function map(tab,f)
    local result = {}
    for k,v in pairs(tab) do
        result[k] = f(v)
    end
    return result
end

Now at this point, with this simple thing, I’m totally sure that map is done. However, it’s best to expand the test a bit, because it explains what the function does.

        _:test("map table", function()
            local tab = {1,2,3,4}
            local mapped = map(tab, function(each) return 2*each end)
            _:expect(#tab).is(4)
            _:expect(mapped[1]).is(2)
            _:expect(mapped[2]).is(4)
            _:expect(mapped[3]).is(6)
            _:expect(mapped[4]).is(8)
        end)

The test runs green, as expected.

I expect that you get the picture now, but let’s do one more function just because. We’ll do select. Here we have to decide whether we’re going to create a compact array or what. Let’s do this:

Create a select function for tables, applying a function on each key-value pair in the table, keeping only those pairs where the function returns true.

I create this whole test:

        _:test("select key-value", function()
            local tab = {a="a", b="B", c="D", d="D"}
            local sel = select(tab, function(k,v) return v==upper(k) end)
            _:expect(sel.a).is(nil)
            _:expect(sel.b).is("B")
            _:expect(sel.c).is(nil)
            _:expect(sel.d).is("D")
        end)

My silly function is to select the pairs where the value is the uppercase equivalent of the key. Test should fail, looking for select.

No! I get this surprise:

2: select key-value -- Tests:27: bad argument #1 to 'select' (number expected, got table)

Apparently there is a built-in function select in Lua. I wonder what it is.

Let’s rename ours to filter and move on. I must remember to look into this but we’re here to demo CodeaUnit.

        _:test("select key-value", function()
            local tab = {a="a", b="B", c="D", d="D"}
            local sel = filter(tab, function(k,v) return v==upper(k) end)
            _:expect(sel.a).is(nil)
            _:expect(sel.b).is("B")
            _:expect(sel.c).is(nil)
            _:expect(sel.d).is("D")
        end)
2: select key-value -- Tests:27: attempt to call a nil value (global 'filter')

That’s more like it. Note that this is one of the useful things about test-driven development … we get information we didn’t expect, as well as information we do expect.

Write filter:

function filter(tab,f)
    local result = {}
    for k,v in pairs(tab) do
        if f(k,v) then
            result[k] = v
        end
    end
    return result
end

Run, expecting green and:

2: select key-value -- Tests:27: attempt to call a nil value (global 'upper')

There is no global function upper. We have to use string.upper:

        _:test("select key-value", function()
            local tab = {a="a", b="B", c="D", d="D"}
            local sel = filter(tab, function(k,v) return v==string.upper(k) end)
            _:expect(sel.a).is(nil)
            _:expect(sel.b).is("B")
            _:expect(sel.c).is(nil)
            _:expect(sel.d).is("D")
        end)

And the tests are green, and filter and map are done. This is the way.

Here are the two CU files, just as I use them as of today. I’ll include the Table file as well. Good luck! I’m going back to the top of this article and work on the Dungeon. See you next time!


CUBase.zip

CodeaUnit.zip

Table.zip

D2.zip