Dungeon 312
I start out to think about that clever trick and my plans for Decor. Then I change direction based on what I’ve learned. This has important implications for estimation, COSMIC or otherwise.
Yesterday I implemented a fluent interface capabiity in makeTables
and zip
, so that rather than type something like this:
local decorTable = makeTables("kind", decors)
local withLoots = zip(decorTable, "loot", loots)
local withTiles = zip(withLoots, "tile", tiles)
You can instead type this:
local withTiles = makeTables("kind", decors):zip("loot", loots):zip("tile", tiles)
Despite the possible long line on the web page, this is, I think much cleaner than creating all those unneeded locals.
To make that work, we needed for the output arrays of both makeTables
and zip
to understand the function zip
. That requires that the arrays have a metatable that tells them two things: first, if you are asked for a key that you don’t have, return whatever that key gives you in the metatable, and, second, for the value “zip” in the metatable, return the function zip
.
So when we send zip
to a table with that metatable, we do not find it. Because we are rigged to check our metatable, we do so, find the function zip
and return it. The metatable setup looks like this:
local result = ... -- whatever
local meta = {}
meta.zip = zip
meta.__index = function(t,k)
return meta[k]
end
setmetatable(result,meta)
return result
We can, of course clean that up so that we don’t create a new metatable for each result. What we have here is the code that showed that we could do it.
The issue in my mind now is:
Your scientists were so preoccupied with whether or not they could, they didn’t stop to think if they should.
– Ian Malcolm
Certainly the fluent notation is cleaner than the temp variable version, and it is quite likely more efficient, not that we’ll be doing enough of this to even measure the difference. But metatables are a bit deep in the bag of tricks, bordering on clever.
There is another issue bearing on the question, namely whether we will really provide this capability using these functions, or whether we will embed them as methods in some kind of factory class that we’ll use to create the arrays of parameter tables that we intend to use to populate the dungeon. Using classes would be more in the style of development we use here chez Ron. I think we’ll lean that way, which will make the fluent interface implementation different, if we choose to do it.
Finally, as I’ve written the SpikeTableZipper and used the functions, I’m not as fond of the basic idea as I once was. Now, we’re assuming, contrary to fact, that there will be some imaginary level-defining Making App, and that using that, the imaginary Level Designers will specify things like the Decor for a level. We’re assuming that the Making App will compile those results and provide the dungeon building code with some kind of convenient tabular representation of what is to be built.
Now if that were truly the case, I’d just assume that the imaginary Making App would return the exact collection of key-value tables to be used in the factory method we already have:
function Decor:fromArrayOfDefinitions(array)
local f = function(def)
return Decor:fromDef(def)
end
return ar.map(array, f)
end
function Decor:fromDef(defTable)
return Decor(defTable.tile, defTable.loot, defTable.kind)
end
Because I don’t expect to build that part of a Making App, I think I’d like to provide at least the level of control over Decor that the makeTables
and zip
function imply, plus a little bit more. Let’s treat the Spike as having given us some learning, and turn to Decor and create a test or three that give us a usable capability to create a level in the style we want.
Let’s get started by looking at how we do it now:
Improving Decor Control
We create decor now, in DungeonBuilder:
function DungeonBuilder:placeDecor(n)
local sourceItems = {
"catPersuasion",
"curePoison",
"pathfinder",
"rock",
"health",
"speed",
"nothing",
"nothing",
"nothing"
}
local loots = {}
for i = 1,n or 10 do
local kind = sourceItems[1 + i%#sourceItems]
local loot = Loot(nil, kind, 4,10)
table.insert(loots, loot)
end
Decor:createRequiredItemsInEmptyTiles(loots,self)
end
The decor loots are all the same in every level, because we always call this with n=30. We then call Decor to do the work:
function Decor:createRequiredItemsInEmptyTiles(loots, runner)
return ar.map(loots, function(loot)
local tile = runner:randomRoomTileAvoidingRoomNumber(666)
return Decor(tile,loot)
end)
end
When we create a Decor without specifying its kind, we get a random kind:
function Decor:init(tile, loot, kind)
self.kind = kind or Decor:randomKind()
self.sprite = DecorSprites[self.kind]
if not self.sprite then
self.kind = "Skeleton2"
self.sprite = DecorSprites[self.kind]
end
self.loot = loot
Bus:publish("moveObjectToTile", self, tile)
self.scaleX = ScaleX[math.random(1,2)]
local dt = {self.doNothing, self.doNothing, self.castLethargy, self.castWeakness}
self.danger = dt[math.random(1,#dt)]
self:initActionSequence()
self.open = false
end
function Decor:randomKind()
return DecorKinds[math.random(1,#DecorKinds)]
end
Using our new scheme we think we want to create an array of n
items, each being a table with keys tile
, loot
, and kind
, and then feed it to our new method fromArrayOfDefinitions
.
I think we’d better start with a test, in Decor, showing how a user object like DungeonBuilder might build what it wants. The test isn’t much about Decor, but if we wanted to know how to create Decor we’d look at its tests, so we’ll start there anyway.
I learn something right away. Here’s what I have so far:
_:test("Build up definition table", function()
local desiredDecor = { Skeleton1=3, Skeleton2=5, Chest=6, BarrelFull=2, BarrelEmpty=2, PotEmpty=1, PotFull=1}
end)
I tried various ways of saying how many of each item I wanted. Candidates included two correlated arrays, which I have been thinking I’d use, a single array of alternating count and string, and this one. The keys are strings, because that’s what the Lua syntax means, and the chosen names are all from the DecorKinds table, unless I’ve made a mistake in any of them.
In writing this test, I’ve already determined a preference for how we specify things, and it’s different from what I imagined when I was thinking about clever ways to manipulate tables.
Let’s move along. There are 20 items in the desiredDecor
, unless I miss my guess, and so we need an array of 20 Loots. Let’s imagine, for now, that random assignment is good enough, not because we can’t do it explicitly, but because absent a tool, it’ll be tedious. It’ll probably be tedious anyway.
No, let’s go explicit. Let’s see. We want there to be exactly one catPersuasion
, some health
, some speed
, and let’s say two pathfinder
, so the new player can waste one, and the experienced player can save one for later. We want to hide the catPersuasion
in a Pot, and the pathFinder
in a Chest.
Now as I think about that, I come to a realization … if I always put the catPersuasion in a Pot, the player will learn that. For now, I’ll accept that, because I could always randomize if I wished. Let’s stick with explicit, like this:
_:test("Build up definition table", function()
local desiredDecor = { Skeleton1=3, Skeleton2=5, Chest=6, BarrelFull=2, BarrelEmpty=2, PotEmpty=1, PotFull=1}
local desiredLoot = {
Skeleton1={"health","speed","nothing"},
Skeleton2={"speed", "nothing", "nothing"},
Chest={ "catPersuasion", "health", "speed", "health", "nothing", "nothing" },
BarrelFull={"speed", "nothing"},
BarrelEmpty={"health", "nothing"},
PotEmpty={"pathfinder"},
PotFull={"nothing"}
}
end)
Again after trying a couple of ideas, I decided just to provide, for each Decor kind, an array of the Loots it should contain. Which, of course, now makes the first keyed table, from kind to count, redundant.
This is one of the big values to TDD. We become the first user of whatever code we’re creating, and we learn, in using it, that our starting idea for it isn’t always the best. So now I can change the test to be like this:
_:test("Build up definition table", function()
local desiredDecor = {
Skeleton1={"health","speed","nothing"},
Skeleton2={"speed", "nothing", "nothing"},
Chest={ "catPersuasion", "health", "speed", "health", "nothing", "nothing" },
BarrelFull={"speed", "nothing"},
BarrelEmpty={"health", "nothing"},
PotEmpty={"pathfinder"},
PotFull={"nothing"}
}
end)
Now I have two-thirds of the information that my new creation methods desire, but in a different form. I still need to specify the tiles. I am happy to have them all be random, except that I don’t want anything in room one.
I could do something like this:
_:test("Build up definition table", function()
local desiredDecor = {
Skeleton1={"health","speed","nothing"},
Skeleton2={"speed", "nothing", "nothing"},
Chest={ "catPersuasion", "health", "speed", "health", "nothing", "nothing" },
BarrelFull={"speed", "nothing"},
BarrelEmpty={"health", "nothing"},
PotEmpty={"pathfinder"},
PotFull={"nothing"}
}
r1 = "r1"
local locations = {
Skeleton1={r1, r1, r1},
Skeleton2={r1,r1,r1},
Chest={ r1,r1,r1,r1,r1,r1},
BarrelFull={r1,r1},
BarrelEmpty={r1,r1},
PotEmpty={r1},
PotFull={r1}
}
end)
The basic notion is that there is some key, “r1”, that means random room tile avoiding room 1, or maybe we’d use the method function or, I don’t know, anything, and when we build the things we use that key to know what function to call.
I don’t like this. I’m not sure why. It would certainly be easy enough to generate from some textual format or data structure in the imaginary Making App. But it’s not easy to type in to test, and it’s not easy if I want to do custom level Decor in code.
Belay that test, let’s try another format.
local desiredDecor = [[
Skeleton1,health,r1
Skeleton1,speed,r1
Skeleton1,nothing,r1
Skeleton2,speed,r1
Skeleton2,nothing,r1
Skeleton2,nothing,r1
Chest,catPersuasion,r1
Chest,health,r1
Chest,speed,r1
Chest,health,r1
Chest,nothing,r1
Chest,nothing,r1
BarrelFull,speed,r1
BarrelFull,nothing,r1
BarrelEmpty,health,r1
BarrelEmpty,nothing,r1
PotEmpty,pathfinder,r1
PotFull,nothing,r1
]]
This was tedious but a program wouldn’t have any trouble creating it from some easier GUI kind of way of defining the situation, if we had one.
This is sending me down an entirely new path, discovered, not so much by thinking about the design but by trying various designs until I find one that fits. I suppose some would-be experts would say that I hacked until I found something livable and they would imagine that they could have just thought better than I can and come up with something at least this good.
Well, good for them. I’ve worked with their designs, and while they look good on paper, they often turn out to be a pain to deal with in the code.
Let’s work a bit, trying this format, to see what we get. I’m going to start with some much simpler testing.
_:test("Decor from definition string", function()
local def = "Skeleton1,health,r1"
local decorKind = Decor:kindFrom(def)
_:expect(decorKind).is("Skeleton1")
decorKind = Decor:kindFrom("skeleton2,foo,bar")
_:expect(decorKind).is("noSuchKind")
end)
I’m deciding here that the new kindFrom
function will return a kind if the input has one, and otherwise “noSuchKind”.
Test will fail on kindFrom
.
11: Decor from definition string -- Decor:391: attempt to call a nil value (method 'kindFrom')
function Decor:kindFrom(aString)
local parsed = {}
for w in string.gmatch(aString, "%w+") do
table.insert(parsed,w)
end
return parsed[1]
end
First cut at the kindFrom. Parses out three words from the string, returns the first. This will fail not finding “noSuchKid”, because “skeleton2” is not a valid kind.
11: Decor from definition string -- Actual: skeleton2, Expected: noSuchKind
Enhance:
function Decor:kindFrom(aString)
local parsed = {}
for w in string.gmatch(aString, "%w+") do
table.insert(parsed,w)
end
return self:legalKind(parsed[1])
end
function Decor:legalKind(proposedKind)
return self:legalKinds()[proposedKind] or "noSuchKind"
end
function Decor:legalKinds()
return {
Skeleton1="Skeleton1",
Skeleton2="Skeleton2",
Chest="Chest",
BarrelEmpty="BarrelEmpty",
BarrelClosed="BarrelClosed",
BarrelFull="BarrelFull",
Crate="Crate",
PotEmpty="PotEmpty",
PotFull="PotFull",
}
end
Test runs. We now have a new method returning legal Decor kinds in a convenient dictionary x->x.
Let’s extend our test. I might even break it in two.
_:test("Decor from definition string", function()
local def = "Skeleton1,health,r1"
local decorKind = Decor:kindFrom(def)
_:expect(decorKind).is("Skeleton1")
end)
_:test("Invalid Decor kind", function()
local decorKind = Decor:kindFrom("skeleton2,foo,bar")
_:expect(decorKind).is("noSuchKind")
end)
Now in the first test, I want to get the Loot. We can give the Loot a nil tile, because the loot will be placed only when the Decor is opened.
_:test("Decor from definition string", function()
local def = "Skeleton1,health,r1"
local decorKind = Decor:kindFrom(def)
_:expect(decorKind).is("Skeleton1")
local loot = Loot:fromDef(def)
end)
This will fail for want of fromDef
. (I am not entirely loving these names, but we’re here to string a rope across the chasm.)
11: Decor from definition string -- Decor:393: attempt to call a nil value (method 'fromDef')
In Loot I see the init
and it tells me that I’ve forgotten something important:
function Loot:init(tile, kind, min, max)
self.kind = kind
self.min = min
self.max = max
self.item = self:createInventoryItem()
if tile then tile:moveObject(self) end
end
We need a min and max in some Loots. health
is one of those. OK, let’s just extend our syntax:
_:test("Decor from definition string", function()
local def = "Skeleton1;health(5,5);r1"
local decorKind = Decor:kindFrom(def)
_:expect(decorKind).is("Skeleton1")
local loot = Loot:fromDef(def)
end)
I’m using semicolon as my main separator, so I can use the comma inside the parens. We’ll need a smarter pattern as well. Not much smarter though.
Back in Loot:
function Loot:fromDef(defString)
-- kind,loot(min,max),tile
local pat = "[^;]+"
local parsed = {}
for w in string.gmatch(defString,pat) do
table.insert(parsed,w)
end
local min, max = 0,0
local minMaxPat = "%w+"
local iter = string.gmatch(parsed[2],minMaxPat)
local kind = iter()
local min = iter() or 0
local max = iter() or 0
return Loot(nil, kind, min, max)
end
I’m not loving this parsing, but I think it works on a well-structured input. We’ll be pulling the parsing out shortly, I expect.
Back to the test:
_:test("Decor from definition string", function()
local def = "Skeleton1;health(5,10);r1"
local decorKind = Decor:kindFrom(def)
_:expect(decorKind).is("Skeleton1")
local loot = Loot:fromDef(def)
_:expect(loot:is_a(Loot)).is(true)
_:expect(loot.kind).is("health")
_:expect(loot.min).is(5)
_:expect(loot.max).is(10)
end)
I expect this to fail on 5 and 10 because they’ll be strings, since I forgot to make them numbers. But with this much rigmarole, some other failure isn’t going to be a huge surprise. Test:
11: Decor from definition string -- Actual: 5, Expected: 5
11: Decor from definition string -- Actual: 10, Expected: 10
Perfect! Fix that:
function Loot:fromDef(defString)
-- kind,loot(min,max),tile
local pat = "[^;]+"
local parsed = {}
for w in string.gmatch(defString,pat) do
table.insert(parsed,w)
end
local min, max = 0,0
local minMaxPat = "%w+"
local iter = string.gmatch(parsed[2],minMaxPat)
local kind = iter()
local min = tonumber(iter() or 0)
local max = tonumber(iter() or 0)
return Loot(nil, kind, min, max)
end
The test runs. We aren’t done, far from it, but it’s lunch time, so I’ve been at this for a full Ron working day, so we’ll wrap up for the day. Commit: Initial tests and implementation for Decor creation from string definition.
Summary
What we have seen over the past few days is an example of what happens when we work incrementally and iteratively. We’ve now taken, what, four or five “days” on the story about controlling the allocation of Decor, which is a sub-story of the larger story about controlling the allocation of all dungeon objects.
We posited a design involving correlated tables, because we saw, or thought we saw, that they’d be easy to create, could be used to describe everything we foresaw wanting to do, and were easy enough to process. That led to the Spike with makeTables
and zip
.
Then, when we started to try to use that idea, it didn’t feel so good. After a few more swings, we’re presently working on a very explicit, written out long-hand, string-driven syntax that will look something like this:
local desiredDecor = [[
Skeleton1;health(5,9);r1
Skeleton1;speed(1,5);r1
Skeleton1;nothing;r1
Skeleton2;speed(2,6);r1
Skeleton2;nothing;r1
Skeleton2;nothing;r1
Chest;catPersuasion;r1
Chest;health(5,10);r1
...
That’s tedious to write, but as easy as anything is likely to be to generate with our never to be implemented anyway GUI system. We’ve determined that we can parse the lines fairly easily, and I predict that we can bullet-proof the parsing to our heart’s desire.
It should be easy, now, to generate the necessary calls to create the actual decor, though we’ll still have to devise something to convert my tag r1
into a room tile of the desired kind. That’s going to be easy from the DungeonBuilder, and not easy from Decor, where we’re working at present. We might decide to move the tests over to DungeonBuilder. Certainly that’s about as reasonable as having them in Decor, since the Builder is the object that wants to do this. Half of one, six a dozen of the other.
Estimation
Recall that we are in conversation with Colin Hammond, ScopeMaster, regarding estimation. He has estimated what I’m doing once and we may agree that he’ll do it better later this week. Or we may agree that it isn’t worth it.
Here’s the rub. Nothing I’ve done here would change the COSMIC Function Points estimate of this little story. It still has the same number of ins and outs and reads and writes, at least as seen from the outside. But I’ve just burned four or five days, and I have maybe a day or two of actual progress. The rest went into learning how to do the job.
Sometimes, given something about this complicated, I know exactly how to do it, and do it. Other times, I don’t know, and it takes a few cuts at it to settle in on a solution. So some stories of this size will take a day or two, and some will take five, six or more. So if we had CFP estimates for this story, our management would be really tempted to wonder why I needed a “whole damn week” to do something I “should have done in a day or two”.
Now Hammond and I, and most any reasonable person, all agree that management shouldn’t do that. Where we may not agree is whether management will do that, and in my experience, all too often they do. It is this observation that makes so many developers, Agile or not, quite reluctant to estimate anything. The estimates are misused way too often.
The variance here is probably not a real concern. Some stories of about the same “functional size” take longer time, some take shorter time, but it will usually average out nicely, especially if we keep our stories small.
What that suggests to me is that even if CFP estimation can pretty accurately assess the “size” of stories, and I suspect that it can, that the difficulty of applying CFP to an Agile effort makes it not worthwhile. The temptation to misuse is too high, and the fact that we do not have all the stories at the beginning means that the overall estimate won’t be very good.
YMMV, and if anyone within sight of my words wants to apply COSMIC Function Points to their effort and report the results, I would welcome finding out more.
Here in the Dungeon, we’ll continue to think about CFP, but mostly we’re just going to evolve our program and discover what happens as we do so.
I hope you’ll stick around to find out.