Dungeon 125
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:
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!
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:
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:
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:
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!