Let’s start on the Dungeon Object allocation story. I think we’ll start in the middle, or near the bottom. So far, no takers from the pro-estimation folx. UPDATE: Hammond has done CFP for the story! Details tomorrow.

I am unsurprised that I haven’t had volunteers from the pro-estimation people, leaping at the opportunity to estimate how long it will take to do the middle-sized story about offering control over how Dungeon Objects are allocated. I’m just as glad about that, because it means that I can follow this estimate, which I think is just about right:

I estimate this task will be completed to your satisfaction when you are done with it, or when something more interesting catches your attention. – Carl Manaster

Anyway … the story is something like

Provide better control over placing Dungeon Objects. Counts, etc1.

Now the theoretical story includes some kind of non-programming Level Designers, who would be given, first, some kind of simple text format for defining dungeon contents, and perhaps later, some convenient graphical tool. Given the limitations of Codea, and the limitations of my interest (thanks, Carl), I doubt that we’ll get to a text format and I feel quite certain that we won’t build a graphical tool. Still, you never know.

My plan today is to start with allocation of Decor containing Loot. What we will be working toward is roughly this. When defining a level of the dungeon, we can specify:

  • How many Decor elements occur in the level;
  • Which specific Decor elements will appear;
  • The proportion of each, or the exact count of each;
  • How many inventory item treasures will occur;
  • Which specific items will occur;
  • The proportion of each, or the exact count of each;
  • Which kinds of damage will be done by decor;
  • How much damage for each kind (a range);
  • What fraction of Decor will deal damage;
  • Where the Decor should occur, room, wall, etc.

Now if I know myself, and I pretty much do, where we’ll get to with this before I consider the problem to be essentially solved, we’ll wind up with pieces of the solution something like this:

  • A decor-creation function that will create a specific Decor kind, with a specific gift in it, and specific damage done.
  • A placement function allowing selection of at least two different kinds of locations, such as in a room or a hallway.
  • It will be possible to specify rooms to avoid (because we already can), and rooms to use (probably).

We’ll need most of this in any case, and much of it we have in embryonic form. In addition, I think we’ll have:

  • Functions to select items from a collection according to a list of proportions, or a list of exact counts.

This last is a bit of design that I’ve done in thinking about this. I’ve not written any code, even on paper (remember paper?), but I’m thinking of a list of things and a corresponding list of proportions and we roll a random number and spin through the list of proportions until we find the index of the thing to place. (A second possibility, and one that I rather like … no, I’ll save it for a surprise.)

We’ll begin by reviewing what we have now, since I certainly don’t remember, and then we’ll probably start with some tests.

Decor Creation Today

All this takes place in DungeonBuilder, for values of “all”. We begin here:

function DungeonBuilder:buildLevel(roomCount)
    self:dcPrepare()
    self:defineDungeonLayout(roomCount)
    self:placePlayer()
    self:customizeContents()
    self:createButtons()
    self:dcFinalize()
    return self.view:asFullDungeon()
end

That of course leads us here:

function DungeonBuilder:customizeContents()
    self:placeSpikes(15)
    self:placeLever()
    --self:placeDarkness()
    self:placeNPC()
    self:placeLoots(10)
    self:placeDecor(30)
    self:placeThings(Key,5)
    self:placeWayDown()
    self:setupMonsters()
end

Thence here:

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

We see that this is looping over loot types, making a table of the desired number of items (the cleverly named n), and then calling a creation method on Decor. We’ll look at that in a moment, but already I can see that we have no control here over what the Decor type is. So in Decor:

function Decor:createRequiredItemsInEmptyTiles(loots, runner)
    return ar.map(loots, function(loot)
        local tile = runner:randomRoomTileAvoidingRoomNumber(666)
        return Decor(tile,loot)
    end)
end

Still no control over the kind of Decor … so:

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

Well! A nice surprise: Decor:init does provide for specifying the kind of decor. As long as we’re here, what are the kinds?

function Decor:randomKind()
    return DecorKinds[math.random(1,#DecorKinds)]
end

local DecorKinds = {
    "Skeleton1","Skeleton1","Skeleton1","Skeleton1","Skeleton1",
    "Skeleton2","Skeleton2","Skeleton2","Skeleton2","Skeleton2",
    "Chest","Chest","Chest","Chest",
    "BarrelEmpty",
    "BarrelClosed",
    "BarrelFull",
    "Crate",
    "PotEmpty",
    "PotFull",
}

Notice that some kinds occur more than once. That gives them a better chance of being chosen. This simple idea is a forerunner of my plan for proportions and exact choices.

I think I have enough background to start working toward our desired level of control. My next phase will be to think in a bit more detail about how I’m going to do this.

Small Up Front Design

When I’m programming, I’m designing all the time. I prefer to do that with code, because the coding “phase” always impacts and abstract design that I may have done. But I do begin by thinking. This morning, I’ll put down some words that are somewhat like my thinking.

We have the ability in Decor to create a Decor item of a particular kind, on a specific tile, with a specific loot. So we could imagine code that loops over a table of items something like this:

{kind=k, loot=l, tile=t}

and just creates the little beggars. And since the creation method is this specific, we can handle any amount of variability or specificity by “simply” controlling what we put in the table.

A sub-task in my brain is wondering whether a raw table as shown above is good, or whether there should be a little DecorSpec class instead. I am inclined to the table, which should raise a flag or two but the reason is this:

When we create a table with keys and values, both are explicit. If we created a DecorSpec, its creation would look like this:

function DecorSpec:init(kind, loot, tile)
    self.kind = kind
    self.loot = loot
    self.tile = tile
end

That’s all well and good, but we have to know the order of the init parameters, and have lost a bit of expressivity there. What would push me toward a class would be if we find that there are operations that DecorSpec needs. Right now, it’s too soon to know. I’ll start with the table, probably.

I think I’d like to have some tests. And I think they’re a new suite, not additional tests in any of the existing suites. Let’s begin with a test.

Test and Code

I have a starting empty test suite in this app, whose name is “RENAME_ME”. I can copy that suite, paste it where I want the tests, and start testing.

function testDecorCreation()
    CodeaUnit.detailed = false
    
    _:describe("Decor Creation", function()
        
        _:before(function()
        end)
        
        _:after(function()
        end)
        
        _:test("First Test", function()
            _:expect(2).is(3) -- change this to fail
        end)
        
    end)
end

I expect this to fail. (Humor?)

1: First Test  -- Actual: 2, Expected: 3

Sure enough, it does. This tells me that my new tests have been found and are being used. Now I have to figure out something to test.

Let’s see. We’re heading toward a table of tile, kind, and loot, created according to desired proportions. (The Level Designer can of course always create specific combinations and add them to our table. We’ll leave that proof until later—perhaps never.)

We’ll have a table entry for each Decor element. So let’s start testing in the direction of a table with a specified number of Decors, with exact counts for each type. What I’m thinking is to create a table of the desired decors, and then plug in loots and then plug in rooms … each according to the selection rules.

I am nearly certain that we’re going to drive out one or more helper objects that give us a selected element from a collection, in some proportion … whatever I mean by that. I am tempted to start building those helpers, but let’s first write a test at the level of what we want.

I’m about to step into a tar pit, testing random behavior. Tests of that kind are difficult to write and unsatisfying when written. I’ll try to avoid that by starting with the “exact” case.

Here’s my first vague cut at the test. I’m still working out how to express the test and the solution:

        _:test("exact count of kinds", function()
            local kinds = {"a", "b", "c"}
            local counts = {3,4,5}
            local expected = {"a", "a", "a", "b", "b", "b", "b", "c", "c", "c", "c", "c"}
            local result = doSomeMagicThing()
            match(result, expected)
        end)

Now I’ll sketch the missing goodies.

local function doSomeMagicThing(kinds, counts)
    assert(#kinds == #counts, "tables must be of equal length")
    local result = {}
    for i,count in ipairs(counts) do
        table.insert(result, kinds[i])
    end
    return result
end

I think that’s just what I want. Note that I added parameters, and I have plugged those into the test as well. Now for match.

Maybe I should run the test? Yeah. Kind of got carried away there. Anyway:

1: exact count of kinds -- Decor:383: attempt to call a nil value (global 'match')

Oh, wow, we need to do match. I want that to loop over an expect:

local function match(actual, expected)
    _:expect(#actual, "table lengths do not match").is(#expected)
    for i,actual in ipairs(actual) do
        _:expect(actual, "position "..i).is(expected[i])
    end
end

I am probably mistaken but I actually expect this test to pass. Fortunately I am quite a bit too optimistic:

1: exact count of kinds table lengths do not match -- Actual: 3, Expected: 12
1: exact count of kinds position 2 -- Actual: b, Expected: a
1: exact count of kinds position 3 -- Actual: c, Expected: a

Hm, what have we wrought here?

        _:test("exact count of kinds", function()
            local kinds = {"a", "b", "c"}
            local counts = {3,4,5}
            local expected = {"a", "a", "a", "b", "b", "b", "b", "c", "c", "c", "c", "c"}
            local result = doSomeMagicThing(kinds, counts)
            match(result, expected)
        end)

That sure looks like we returned just three items …

Oh haha, this is wrong:

local function doSomeMagicThing(kinds, counts)
    assert(#kinds == #counts, "tables must be of equal length")
    local result = {}
    for i,count in ipairs(counts) do
        table.insert(result, kinds[i])
    end
    return result
end

I forgot to put in count copies. See why we test?

local function doSomeMagicThing(kinds, counts)
    assert(#kinds == #counts, "tables must be of equal length")
    local result = {}
    for i,count in ipairs(counts) do
        for j = 1,count do
            table.insert(result, kinds[i])
        end
    end
    return result
end

The test runs now. Let’s see about a better name for that magical function. I’ll leave it here as a function for now.

local function expandByCount(kinds, counts)
    assert(#kinds == #counts, "tables must be of equal length")
    local result = {}
    for i,count in ipairs(counts) do
        for j = 1,count do
            table.insert(result, kinds[i])
        end
    end
    return result
end

That seems like a decent name. Extend might be better than expand, but good enough for now.

I’m wondering about randomness. If we continue down this path, we’re going to wind up with a table of kinds in order, containing loots in order, and damage in order. That would mean that the first decors would get all the good stuff, and could lead to something like boxes never containing anything good.

Let’s imagine the creation scenario a bit. Then I’ll see if I can fiddle up a test to do it.

We choose which kinds we want in a given level. Maybe just vases and boxes. We choose how many of each. We generate the expanded list, which is now a perfect prototype for what we’re going to feed into the decor creation loop. It just needs the loots, damage, and rooms. “Just”.

(I am reminded that we’re working toward a table of named items … we’ll need not to forget but I decide not to address it yet.)

Anyway, with the exact list of Decor kinds in hand, we want to put into each one, an appropriate loot. I think “nothing” is always a valid loot, by the way. Imagine that what we want for this level is:

  1. Ten Decor, created as above.
  2. Exactly one Mysterious Rock of Dullness.
  3. Five other loots, either Health or Speed, in proportion 3:2.
  4. The rest, nothing.
  5. Seven of the Decor do damage, four to Health, two to Speed, and one poison, randomly assigned.

So. Let’s think about solutions. For zipping in these other items, loots and damage, maybe we’ll create lists of the same length as the Decor list, which we’ll consider to be the controlling list. Hmm …

Pardon the randomness, we’re tracking my thoughts here …

To select one Rock or five Health or Speed in proportion 3:2, I can think of a couple of approaches:

  1. Create one Rock, 3 Healths and 2 Speeds in an array, and draw randomly without replacement. But this would, again, put all the good stuff in the first items in the control list;
  2. Do the above but draw randomly (without replacement) from the control list, then draw without replacement from the gift list;
  3. Create lists of the correct length, including null items, draw randomly without replacement.
Without replacement?
What I mean here is this. Suppose we have a list of 3 Healths and 2 Speeds. If we draw from the list with replacement, that means that we select a random item but we don’t remove it from the list. So with a little luck you could get 5 Healths.

Drawing without replacement, we would remove whatever we select from the list, so that if we draw a Health first, the list will then contain 2 H and 2 S, and so on.

Either way is easy, especially at the scale of lists we’ll be using here with just a few elements. What I’m most concerned about is how best to express what we are trying to accomplish.

Hm. It’s like we’re going to have a few arrays. One will be the exact array of Decor kinds we want, since we know how to do that. Then we’ll have an array of the exact Loots we want. We’ll create a new table by withdrawing a random element from the first table, withdrawing a random element from the second table (with or without replacement, an option), and putting those two items, decor and loot, together, to produce the next version of the Decor array. Then we can do the thing again with an array of damage.

There’s an object taking shape here, some kind of table maker, or table extender, or expander …

I’m going to commit what we have here and do a Spike so that I can get a sense of this. Commit: Start at Decor Creation test and code.

Now to a new little program. I start it with a CodeaUnit base, in hopes of behaving well, even though this is a spike and I can do what I want, so there.

The code we’ll ultimately want will be probabilistic, but I’ve got a half an idea about that.

I start with this:

        _:test("zip without replacement", function()
            local control = {"a", "b", "c", "d", "e", "f"}
            local without = { "y", "y", "z"}
            local kinds = makeTables("kind", control)
            dump(kinds)
        end)

    end)
end

function makeTables(key,tab)
    local result = {}
    for i,v in ipairs(tab) do
        local entry = {}
        entry[key]=v
        table.insert(result, entry)
    end
    return result
end

function dump(tab)
    local report = ""
    for i,t in ipairs(tab) do
        for k,v in pairs(t) do
            report = report..k..": "..v.."\n"
        end
        report = report.."\n"
    end
    print(report)
end

That prints this:

kind: a

kind: b

kind: c

kind: d

kind: e

kind: f


I can paste that into a test, I imagine. Some futzing and I get this test:

        _:test("zip without replacement", function()
            local control = {"a", "b", "c", "d", "e", "f"}
            local without = { "y", "y", "z"}
            local kinds = makeTables("kind", control)
            local d = dump(kinds)
            print(d)
            local expected = [[kind: a$$kind: b$$kind: c$$kind: d$$kind: e$$kind: f$$]]
            _:expect(d).is(expected)
        end)

And this dump:

function dump(tab)
    local report = ""
    for i,t in ipairs(tab) do
        for k,v in pairs(t) do
            report = report..k..": "..v.."$"
        end
        report = report.."$"
    end
    return report
end

Weird but getting there. I have created a table for each starting entry. Each of those tables has a key of “kind” and a value from the input table. Now let’s try a simple zipper function.

        _:test("zip without replacement", function()
            local control = {"a", "b", "c", "d", "e", "f"}
            local without = { "y", "y", "z"}
            local kinds = makeTables("kind", control)
            local d = dump(kinds)
            --print(d)
            local expected = [[kind: a$$kind: b$$kind: c$$kind: d$$kind: e$$kind: f$$]]
            _:expect(d).is(expected)
            local zip = {"u", "v", "w", "x", "y", "z"}
            local zipped = zip("loot", kinds, zip)
        local zipexp = [[kind: a$loot: u$$kind: b$loot: v$$kind: c$loot: w$$kind: d$loot: x$$kind: e$loot: y$$kind: f$loot: z$$]]
            print(zipped)
            _:expect(zipped).is(zipexp)
        end)

Yes, I actually typed in zipexp so I have high hopes for it but fear as well. This requires zip of course:

1: zip without replacement -- Tests:22: attempt to call a table value (local 'zip')

Oops, can’t call the table and the function the same.

            local zipin = {"u", "v", "w", "x", "y", "z"}
            local zipped = zip("loot", kinds, zipin)

Right, now it demands zip, which I code:

Oh … in coding this, I realize that since tables are passed by value, I need to copy the input tables before zipping in the other bits. Glad I thought of that.

I try this:

function zip(key, t1, t2)
    assert(#t1==#t2, "tables must have same length")
    local result = {}
    for i,t in ipairs(t1) do
        local element = {}
        for k,v in pairs(t) do
            element[k] = v
        end
        element[key] = t2[i]
    end
    return result
end

Test. I’m really glad I’m writing tests for this.

1: zip without replacement  -- Actual: table: 0x284138040, Expected: kind: a$loot: u$$kind: b$loot: v$$kind: c$loot: w$$kind: d$loot: x$$kind: e$loot: y$$kind: f$loot: z$$

Forgot to dump.

        _:test("zip without replacement", function()
            local control = {"a", "b", "c", "d", "e", "f"}
            local without = { "y", "y", "z"}
            local kinds = makeTables("kind", control)
            local d = dump(kinds)
            --print(d)
            local expected = [[kind: a$$kind: b$$kind: c$$kind: d$$kind: e$$kind: f$$]]
            _:expect(d).is(expected)
            local zipin = {"u", "v", "w", "x", "y", "z"}
            local zipped = zip("loot", kinds, zipin)
        local zipexp = [[kind: a$loot: u$$kind: b$loot: v$$kind: c$loot: w$$kind: d$loot: x$$kind: e$loot: y$$kind: f$loot: z$$]]
            local zipdump = dump(zipped)
            print(zipdump)
            _:expect(zipdump).is(zipexp)
        end)

Test hopefully.

1: zip without replacement  -- Actual: , Expected: kind: a$loot: u$$kind: b$loot: v$$kind: c$loot: w$$kind: d$loot: x$$kind: e$loot: y$$kind: f$loot: z$$

Looks a lot like we didn’t return anything. A quick assert confirms that. Recheck zip:

function zip(key, t1, t2)
    assert(#t1==#t2, "tables must have same length")
    local result = {}
    for i,t in ipairs(t1) do
        local element = {}
        for k,v in pairs(t) do
            element[k] = v
        end
        element[key] = t2[i]
    end
    return result
end

It would help to put the element into the result:

function zip(key, t1, t2)
    assert(#t1==#t2, "tables must have same length")
    local result = {}
    for i,t in ipairs(t1) do
        local element = {}
        for k,v in pairs(t) do
            element[k] = v
        end
        element[key] = t2[i]
        table.insert(result,element)
    end
    return result
end

This might be easier with my FP array functions, but I’m not ready to relearn them, nor to insert their complexity into what I’m doing here. I expect the test to run modulo a typo in my input … oh … except that I can’t guarantee the key order … let’s see.

1: zip without replacement  -- 
Actual: loot: u$kind: a$$loot: v$kind: b$$loot: w$kind: c$$loot: x$kind: d$$loot: y$kind: e$$loot: z$kind: f$$, 
Expected: kind: a$loot: u$$kind: b$loot: v$$kind: c$loot: w$$kind: d$loot: x$$kind: e$loot: y$$kind: f$loot: z$$

Right you are. Improve dump:

function dump(tab)
    local report = ""
    for i,t in ipairs(tab) do
        local keys = {}
        for k,_ in pairs(t) do
            table.insert(keys,k)
        end
        table.sort(keys)
        for i,k in ipairs(keys) do
            local v = t[k]
            report = report..k..": "..v.."$"
        end
        report = report.."$"
    end
    return report
end

Retrieve and sort the keys, then use them. Nasty but there it is. And the test runs.

This means I can take an array of tables with keyed entries, and zip in another table of the same length, providing the key name to be used.

But the testing is a bear. Maybe if I test at the element level?

        _:test("one zip step", function()
            local kind = {kind="a", pain="intense"}
            local loot = "aLoot"
            local zipped = zipPair("loot", kind, loot)
            _:expect(zipped.kind).is("a")
            _:expect(zipped.pain).is("intense")
            _:expect(zipped.loot).is("aLoot")
        end)

Our control table is a table with one or more keyed values, but our zipped-in table is just a list of objects or values, as yet unkeyed.

So this will fail looking for zipPair:

2: one zip step -- Tests:33: attempt to call a nil value (global 'zipPair')

We willingly provide:

function zipPair(key, keyedTable, value)
    local result = clone(keyedTable)
    result[key] = value
    return result
end

function clone(keyedTable)
    local result = {}
    for k,v in pairs(keyedTable) do
        result[k] = v
    end
    return result
end

I decided to break out the clone.

By the way, I think in actual use, we could get away with adding elements into our control table rather than copying them each time. We’ll think about that. It does seem kind of wasteful to create these little tables and then throw them away. But the thing is, we don’t really know what the users of these low-level functions are doing, so I think we dare not assume. (You know why.)

I think this test should run, but I am frequently mistaken.

It does run. Let’s use this new stuff in the zipping method.

function zip(key, t1, t2)
    assert(#t1==#t2, "tables must have same length")
    local result = {}
    for i,t in ipairs(t1) do
        local element = zipPair(key,t, t2[i])
        table.insert(result,element)
    end
    return result
end

My tests all run. Let’s reflect a bit on what we’ve learned here.

Reflection

Well, we’ve certainly got some functions that can zip together matching tables. And we’ve got the element creation down to a science, we just pass in the new key, the current version of the output element, and the new item to be added and create a new table with the old keyed items plus our one new one.

We’ve also noticed that testing the details of all the elements in a result table is horrid. I managed to glom out a string representation that is nearly possible to type in, at the expense of sorting keys and stringing together weird characters.

I’d like to have a better way of being sure that the zip part of the thing works, ideally without typing in a nearly impossible string.

I mentioned my FP operators, which include map, filter, reduce kinds of things.

What if we were to bring those in?

dependencies showing fp

However … reviewing the fp functions, I find that the map doesn’t provide the index in the function we call. We can get around that … let me try.

function zip(key, t1, t2)
    assert(#t1==#t2, "tables must have same length")
    local ary = Array(t1)
    local i = 0
    return ary:map(function(t1Entry)
        i = i + 1
        return zipPair(key,t1Entry, t2[i])
    end)
end

Is that too obscure? How about this:

function zip(key, t1, t2)
    assert(#t1==#t2, "tables must have same length")
    local i = 0
    return ar.map(t1, function(t1Entry)
        i = i + 1
        return zipPair(key,t1Entry, t2[i])
    end)
end

Same thing, just explicit fp call. This code isn’t notably better than what we had. I don’t think the FP bears its weight here. We’ll stick with this:

function zip(key, t1, t2)
    assert(#t1==#t2, "tables must have same length")
    local result = {}
    for i,t in ipairs(t1) do
        local element = zipPair(key,t, t2[i])
        table.insert(result,element)
    end
    return result
end

We could inline the local element but I’m happy this way. And a bit sad that map didn’t help me.

Let’s improve the names a bit here, OK?

function zip(key, keyedTableArray, valueArray)
    assert(#keyedTableArray==#valueArray, "tables must have same length")
    local result = {}
    for i,keyedTable in ipairs(keyedTableArray) do
        local element = zipPair(key, keyedTable, valueArray[i])
        table.insert(result,element)
    end
    return result
end

Just renamed a few things. Now I see that the zipPair function is more specific than that. Rename and reorder parameters:

function zip(key, keyedTableArray, valueArray)
    assert(#keyedTableArray==#valueArray, "tables must have same length")
    local result = {}
    for i,keyedTable in ipairs(keyedTableArray) do
        local element = cloneWithNewKeyedValue(keyedTable, key, valueArray[i])
        table.insert(result,element)
    end
    return result
end

function cloneWithNewKeyedValue(keyedTable, key, value)
    local result = clone(keyedTable)
    result[key] = value
    return result
end

I feel better about those names, although that last one, cloneWithNewKeyedValue is rather long and specific. But it’s correct.

It is now 1215. I started at about 0845. It’s a good time to stop, so let’s sum up.

Summary

One thing to notice. Almost every time when I do reflection, I come up with a question or idea that sets me right back to coding. That’s not a bug, it’s a feature. We pull out head out of the details, we look around, we get fresh thoughts, and we get right back to it.

Now then …

What has happened today? Well, we’re converging on a design that provides a table of entries to Decor creation, each entry including a few keyed values indicating the kind of Decor to create, the loot to associate with it, the pain it should cause, and the tile upon which it should be placed.

What we have right now is the ability, in an early form, to create an array of selected Decors, and given arrays of the same length containing loots, pain, or tiles, to zip them together. It should be trivial to take that combined zipped up array of tables and punch out the necessary layout. If we went to the trouble of inserting very specific tiles into there, we could put any given Decor exactly where we wanted it.

We might do that just to prove a point. Or not.

None of this is integrated into D2 yet, and we’ve not yet actually worked out any of the random selection methods we’ll need. But given the notion of an array of the right size, I have a good feeling about those … even though I only have a vague idea about how to do it, I’m sure that we can.

Estimation

We’ve burned another Ron-day on our story. We’ve made discernible progress toward what feels like a quite decent design. I have no idea how many COSMIC Function Points we have just created, if any. Do I have any idea how many more Ron days are needed to do this?

I’ll try. Suppose that what we’re working toward is to be able to choose the types of Decor by exact count (10). Suppose we want to choose how many will deliver pain, typed by exact count (3 Health, 2 Speed, 1 Poison, 4 Nothing). Suppose we want 4 Loots (1 Rock, 1 Cat Persuasion, 2 Health). And we want them all on randomly chosen room tiles, just to keep it easy.

I think how I’d like it to work is to provide, say, the pain in this form:

pain = (Health, Speed, Poison)
count = (3,2,1)
default = Nothing

Or maybe

pain = { Health, 3, Speed, 2, Poison, 1}
default = Nothing

And then we either generate an actual zipping array, or we have some kind of smart object that acts like it has the right length. It would have to work without replacement, I think.

I think it’s probably two more sessions at most to make it work with all the same sized arrays, which one could type in manually. Adding in the selecting by ratio and such, seems it couldn’t take me more than three sessions.

I think I can estimate that I’ll have this done, at the earliest in four days, and with 90% confidence, six. Unless I learn something bad.

Would I promise that? Well, the 90% helps me out, I can always say, “Well, bad roll of the ten-sided, the bear bit us, I said there was an outside chance it would take longer”.

But the core thing is this: Assuming this estimate is fairly decent, and I think it is, I needed to do today’s work before I could even get that close. And, frankly, my subjective feeling was that I’d get further than I got today. The FP experiment took a bit of the time, but I’d argue that it needed to be done, because we do have the FP capability, so it’s a legitimate question as to whether to use it.

This is how iterative incremental development goes. We make good progress every day, and we’ll work on whatever you want us to work on … and we don’t know how long it will take until we’re well into it. But everyone can see the progress and decide what to do about it.

I’m sure it would be more satisfying to have a solid timeline for this product. And maybe one of my betters could do it. I don’t see how, but I’m willing to learn if someone wants to step up.

The problem they’ll have is that they’ll want to ask me questions that I don’t know the answers to. To answer them, I must design. And to design, I pretty much need to code, unless you want the answers for “how long” and “what will you do” to both be wrong a good part of the time.

Do you feel lucky?



  1. With my handwriting, one can never be quite sure what a yellow sticky note says. I can usually decode enough to get the drift.