Let’s work again, on controlling the dungeon layout. My objective this morning is to define at least one level’s Decor. I’m extending the story based on feedback.

The feedback came from someone I trust: me. I found it tedious in the extreme to specify the details of each Decor item and its Loot, and I haven’t even begun on the question of specifying the associated damage that I’m calling “pain”. If I recall correctly, we do have the ability to specify a level’s Decor in complete detail. My plan this morning is to specify it roughly, but not down to which item has which Loot. It’s be somewhat random, but in a desirable way. I know it’s desirable, because I desire it.

Review

Let’s refresh our memories. Here are the tests that show that explicit control works:

        _:test("Create from definition table", function()
            local def = {tile="tile", loot="loot", kind="BarrelEmpty"}
            local decor = Decor:fromDef(def)
            _:expect(decor.loot).is("loot")
            _:expect(decor.kind).is("BarrelEmpty")
            _:expect(Bus:count("moveObjectToTile", "did not move to tile")).is(1)
            local parms = Bus.lastParameters
            _:expect(parms[1]).is(decor)
            _:expect(parms[2]).is("tile")
        end)
        
        _:test("Create from array of definition table", function()
            local def1 = {tile="tile", loot="loot", kind="BarrelEmpty"}
            local def2 = {tile="tile", loot="loot", kind="BarrelFull"}
            local decors = Decor:fromArrayOfDefinitions({def1,def2})
            _:expect(#decors,"didn't get 2").is(2)
            _:expect(Bus:count("moveObjectToTile", "did not move to tile")).is(2)
            _:expect(decors[1]:is_a(Decor)).is(true)
            _:expect(decors[2]:is_a(Decor)).is(true)
            _:expect(decors[1].kind).is("BarrelEmpty")
            _:expect(decors[2].kind).is("BarrelFull")
        end)

We accept a single definition, a keyed table containing keys tile, loot, and kind, and create a suitable Decor. And we can create an array of such definitions and create them all. Yes, I only tested two. But I also wrote the code, so I know that it isn’t limited to two.

The Plan

My plan is to provide input that looks like the code below … no, let me sketch it right in a test:

        _:test("array shorthand", function()
            local short = {3,"Chest", 4, "Skeleton1", 2, "BarrelEmpty"}
            local long = extend(short)
            _:expect(#long).is(3+4+2)
        end)

My plan, as I’m sure you saw, is that we’ll wind up with 3 Chests, 4 Skeleton1, and 2 BarrelEmpty. I’m going to write the function in line for now.

Test should fail looking for extend.

11: array shorthand -- Decor:409: attempt to call a nil value (global 'extend')

Code it to exist:

function extend(countItemArray)
    
end

Should fail with nil not equaling 9. Not quite, but I’ll allow it:

11: array shorthand -- Decor:410: attempt to get length of a nil value (local 'long')

Let’s return an array, just for tiny steps.

function extend(countItemArray)
    return {}
end

Now I definitely expect 0 not to equal 9.

11: array shorthand  -- Actual: 0, Expected: 9

OK let’s do the work.

I should mention that a true fanatic might have written the first test just to return one segment of the array. And, you know what, that sounds so interesting that I’m going to rewrite my test to do that. In a moment we’ll see why.

Let me preface my new test with my original plan for the extend function. My thought was that I’d iterate through the array in steps of two and do the obvious thing. But what if this was the test:

        _:test("array shorthand", function()
            local short = {3,"Chest"}
            local long = extend(short)
            _:expect(#long).is(3)
            _:expect(long[1]).is("Chest")
            _:expect(long[2]).is("Chest")
            _:expect(long[3]).is("Chest")
        end)

Here I’ve just done one pair. Why is this a better test? Well, it’s smaller, and that’s generally good. But it has a property that surprised me when I first wrote a few paragraphs above, about writing the one segment test:

It makes me wonder if there is an easy way to do a segment at a time, other than the loop by 2, which you have to admit is rather oddly specific.

What if our extend method returned two arrays? One would be the extended array for the first pair in the input, and the second would be the rest of the input. Let’s try that.

Let’s extend the test to express that:

        _:test("array shorthand", function()
            local short = {3,"Chest"}
            local long = extendSegment(short)
            _:expect(long[1]).is("Chest")
            _:expect(long[2]).is("Chest")
            _:expect(long[3]).is("Chest")
        end)

No … let’s go simpler, assuming a function extendSegment, not extend (yet).

function extendSegment(countItemArray)
    result = {}
    for i = 1,countItemArray[1] do
        table.insert(result, countItemArray[2])
    end
    return result
end

Test passes. Let’s go a step further, in a new test, and now use our intended function name extend:

        _:test("array shorthand extend once", function()
            local short = {3,"Chest"}
            local long, residual = extend(short)
            _:expect(long[1]).is("Chest")
            _:expect(long[2]).is("Chest")
            _:expect(long[3]).is("Chest")
            _:expect(#residual).is(0)
        end)

Now I’m asking this new function, extend, to return two results. The first seems to be the result of extendSegment and the second is empty. Should be small enough:

function extend(countItemArray)
    return extendSegment(countItemArray), {}
end

I expect this to pass. It does. Let’s do another test, with two pairs in the input:

        _:test("array shorthand extend twice", function()
            local long, residual
            local short = {3,"Chest",4,"Skeleton1"}
            long,residual = extend(short)
            _:expect(#long).is(3)
            _:expect(long[1]).is("Chest")
            _:expect(long[2]).is("Chest")
            _:expect(long[3]).is("Chest")
            _:expect(#residual).is(2)
            long,residual = extend(residual)
            _:expect(#long).is(2)
            _:expect(long[1]).is("Skeleton1")
            _:expect(long[2]).is("Skeleton1")
            _:expect(#residual).is(0)
        end)

Here I’m expressing that I get my three chests and that the residual has two items in it, and when I extend that, I get two skeletons and no residual. Test should fail.

13: array shorthand extend twice  -- Actual: 0, Expected: 2
13: array shorthand extend twice -- Decor:449: bad 'for' limit (number expected, got nil)

The first error says we didn’t return any contents in residual. The second reflects the fact that we called extendSegment with an empty array. Fix extend:

I try to write this with table.move, which I haven’t ever used, to my recollection. It keeps failing:

function extend(countItemArray)
    local extended = extendSegment(countItemArray)
    local residual = {}
    table.move(countItemArray, 3,#countItemArray-2, 1, residual)
    return extended, residual
end

This version fails:

13: array shorthand extend twice  -- Actual: 0, Expected: 2
13: array shorthand extend twice -- Decor:452: bad 'for' limit (number expected, got nil)

Before I do anything else, I decide to write this out longhand:

function extend(countItemArray)
    local extended = extendSegment(countItemArray)
    local residual = {}
    for i = 3,#countItemArray do
        table.insert(residual, countItemArray[i])
    end
    return extended, residual
end

That results in this:

13: array shorthand extend twice  -- Actual: 4, Expected: 2

It seems I asked for 4 skels and checked for 2. Change the test to be correct:

        _:test("array shorthand extend twice", function()
            local long, residual
            local short = {3,"Chest",2,"Skeleton1"}
            long,residual = extend(short)
            _:expect(#long).is(3)
            _:expect(long[1]).is("Chest")
            _:expect(long[2]).is("Chest")
            _:expect(long[3]).is("Chest")
            _:expect(#residual).is(2)
            long,residual = extend(residual)
            _:expect(#long).is(2)
            _:expect(long[1]).is("Skeleton1")
            _:expect(long[2]).is("Skeleton1")
            _:expect(#residual).is(0)
        end)

This should work. And it does. I am now confident in my extend function. I’m not sure, however, that it does what I really want, because we really want to end up with a single array with 3 chests and 2 skeletons. Let’s write that out longhand in another test like the latest one:

        _:test("array shorthand extend all", function()
            local long, residual
            local short = {3,"Chest",2,"Skeleton1"}
            long = extendAll(short)
            _:expect(#long).is(5)
            _:expect(long[1]).is("Chest")
            _:expect(long[2]).is("Chest")
            _:expect(long[3]).is("Chest")
            _:expect(long[4]).is("Skeleton1")
            _:expect(long[5]).is("Skeleton1")
        end)

I’m positing yet another function, extendAll. Test should fail for want of it.

13: array shorthand extend all -- Decor:427: attempt to call a nil value (global 'extendAll')

Let’s write extendAll, if we can … and I reckon we can.

function extendAll(countItemArray)
    local entries, residual
    local result = {}
    residual = countItemArray
    while (#residual) > 0 do
        entries,residual = extend(residual)
        table.move(entries,1,#entries, #result+1, result)
    end
    return result
end

I swear this is just as I wrote it the first time. And the test runs!

Let’s extend it just for fun, to three pairs.

        _:test("array shorthand extend all", function()
            local long, residual
            local short = {3,"Chest",2,"Skeleton1", 4, "BarrelEmpty"}
            long = extendAll(short)
            _:expect(#long).is(9)
            _:expect(long[1]).is("Chest")
            _:expect(long[2]).is("Chest")
            _:expect(long[3]).is("Chest")
            _:expect(long[4]).is("Skeleton1")
            _:expect(long[5]).is("Skeleton1")
            _:expect(long[6]).is("BarrelEmpty")
            _:expect(long[7]).is("BarrelEmpty")
            _:expect(long[8]).is("BarrelEmpty")
            _:expect(long[9]).is("BarrelEmpty")
        end)

I expect this to run. It does. Let’s cut a save point. Commit: extendAll function for Decor definition.

Let’s reflect a bit.

Reflection

Now I am still sure that I could have written out this whole extendAll function longhand. I even choose to believe that it wouldn’t have required a lot of debugging. But instead, I thought about how Ted M. Young would probably write a tiny test, and that made me think that a tiny test might generate a tiny simple method, so I went down that path. And the result of that path is these three simple functions:

function extendAll(countItemArray)
    local entries, residual
    local result = {}
    residual = countItemArray
    while (#residual) > 0 do
        entries,residual = extend(residual)
        table.move(entries,1,#entries, #result+1, result)
    end
    return result
end

function extend(countItemArray)
    local extended = extendSegment(countItemArray)
    local residual = {}
    for i = 3,#countItemArray do
        table.insert(residual, countItemArray[i])
    end
    return extended, residual
end

function extendSegment(countItemArray)
    result = {}
    for i = 1,countItemArray[1] do
        table.insert(result, countItemArray[2])
    end
    return result
end

Speaking of those, let’s see if we can recast that loop in extend to table.move. Sure enough, this time I get it right:

function extend(countItemArray)
    local extended = extendSegment(countItemArray)
    local residual = {}
    table.move(countItemArray,3,#countItemArray,1,residual)
    return extended, residual
end

Commit: use table.move in extend function.

As I was saying, we got three simple functions instead of what would surely have been one complicated one, with at least two loops in it, and possibly more. The smaller case gave my brain time to think about table.move, which might not have come to mind if I was thinking in terms of loops.

I believe that I got a better design by doing a better job of thinking of the smallest reasonable test. As soon as I thought of the smaller test, I was able to see enough of what was going to happen to be sure I should try it. But even if I had not seen the likely better result … I’d still have gotten a better result, just by virtue of taking Many More Much Smaller Steps.

Let’s get back to the story:

The Story

The story is something about creating an array of Decor, explicitly, as we’ve just done, and then providing an array of Loot, of the same size, which are to be matched randomly with the Decor array, and ultimately dumped into a table of those definitions with tile, kind, and loot keys.

I think we’re working toward some kind of DecorFactory here, but let’s continue with functions. I think I’d like to start with the random matching part. Let’s see if we can follow a similar pattern to the extend one.

Now if I could make a randomly ordered table of Loot, I could just zip them together. But all I know how to do now is to produce a table that packs all the same kind together. Let’s see if we can make a randomly ordered table. That’s going to be hard to test very well, but we’ll see what we can do.

        _:test("randomly ordered table", function()
            local tab = {1,2,3,4,5,6,7,8}
            math.randomseed(1357)
            local ran = randomize(tab)
            local str = stringFrom(ran)
            _:expect(str).is("12345678")
        end)

By setting the seed, I should ensure that whatever my randomize does, it will do it consistently. I’m assuming my stringFrom will append all the values together in order. They won’t be 12345678, or they’d better not, but once I know the value I’ll put it in the test. This is what LlewellynFalco and Arlo Belshee refer to as Approval Tests, tests where you check the output once by hand and then put that result into the test for the future.

We have a bit of code to write. Test should fail looking for randomize.

14: randomly ordered table -- Decor:443: attempt to call a nil value (global 'randomize')

Code that:

function randomize(array)
    local copy = {}
    table.move(array,1,#array,1,copy)
    local result = {}
    while #copy > 0 do
        local itemNr = math.random(#copy)
        table.insert(result,copy[itemNr])
        table.remove(copy,itemNr)
    end
end

I copied the input array, because otherwise my loop would destroy the input array. Lua passes tables by reference. I think I dodged a bullet here. Test should fail looking for stringFrom, I think.

14: randomly ordered table -- Decor:444: attempt to call a nil value (global 'stringFrom')

Write that:

function stringFrom(array)
    return ar.reduce(array, "", function(str,item) return str..tostring(item) end)
end

That got written quickly. Test failed because I forgot to return the random result. Test is now:

        _:test("randomly ordered table", function()
            local tab = {1,2,3,4,5,6,7,8}
            math.randomseed(1357)
            local ran = randomize(tab)
            local str = stringFrom(ran)
            _:expect(str).is("12345678")
        end)

Should fail with a specific string that I can use:

14: randomly ordered table  -- Actual: 56218734, Expected: 12345678

Sweet. I declare that to be random. Copy to the test. Test runs. Commit: randomize function returns new randomized table.

Zipped Table

OK, let’s go for a zipped table of tables. I’ll ignore the tile parm for now.

Hmm, this gets tricky. Let me show you what I’ve got so far:

        _:test("zip decor and loots of same length", function()
            local decor = {"Chest","BarrelEmpty","Skeleton1"}
            local loots = {"health(4,4)", "speed(1,1)", "catPersuasion"}
            local defs = zip(decor,loots)
            _:expect(defs[1].decor.kind).is("Chest")
            _:expect(defs[1].loot.kind).is("health")
        end)

The thing is this: We have this nice Parse object that expects those strings separated by semicolons. So this isn’t what we want when we zip. We want a collection of those strings.

Nice, that should be easier.

I think this is close to what we want

        _:test("zip decor and loots of same length", function()
            local decor = {"Chest","BarrelEmpty","Skeleton1"}
            local loots = {"health(4,4)", "speed(1,1)", "catPersuasion"}
            local defs = zip(decor,loots)
            _:expect(defs[1]).is("Chest;health(4,4);avoid(1)")
            _:expect(defs[2]).is("BarrelEmpty;speed(1,1);avoid(1)")
            _:expect(defs[3]).is("Skeleton1;catPersuasion;avoid(1)")
        end)

We’ll fail for want of zip:

15: zip decor and loots of same length -- Decor:451: attempt to call a nil value (global 'zip')

Implement:

function zip(string1, string2)
    return string1..";"..string2..";avoid(1)"
end

I expect a pass. I am a fool, I forgot the loop. Should have done the tiny test. This time I’ll write it out:

function zip(array1, array2)
    local result = {}
    for i = 1,#array1 do
        table.insert(result, array1[i]..";"..array2[i]..";avoid(1)")
    end
    return result
end

Test passes. Commit: zip function takes decor name and loot name tables and zips to parseable form.

OK, I need a break. It’s 1050 and I started at 0837. Let’s rest and think.

Rest and Reflect

I think we have enough pieces to allow us to take a couple of those pair lists, one for decor and one for loot, extend them, randomize the loots, and zip them together to get a control array for creation of a level’s decor. I think I’ve been keeping more in my head than I should, because my head feels tired. But we’re so close, I am tempted to finish the story today.

We have a method in DungeonBuilder, tested, that creates Decor from a table of definitions:

function DungeonBuilder:fromDefs(defArray)
    local function parseItem(defString)
        return Parse:decorDefinition(defString)
    end
    local function valid(parsedItem) return parsedItem.valid end
    local function fromParsed(parsedItem) return self:fromParsed(parsedItem) end
    
    return Array(defArray):map(parseItem):filter(valid):map(fromParsed)
end

Are these guys actually created into the dungeon? I think they are, because the Decor are given a tile in the fromParsed:

function DungeonBuilder:fromParsed(parsed)
    local decorKind = Decor:legalKind(parsed.decor.kind)
    local loot = Loot(nil, parsed.loot.kind, parsed.loot.min, parsed.loot.max)
    local tile = self:findTile(parsed.room.kind, parsed.room.p[1])
    return Decor(tile, loot, decorKind)
end

So that says to me that if I were to gin up some tables like we’ve been using here, we would get a level with them in it. Let’s try this:

function DungeonBuilder:placeDecor(ignored)
    local decor = {"Chest", "Chest", "Chest"}
    local loot = {"health(4,4)", "speed(1,1)", "catPersuasion"}
    local parsed = zip(decor,loot)
    self:fromDefs(parsed)
end

I’m just overriding the regular placeDecor. Let’s see what blows up. Nothing blows up. I use the Dev View of the map to show me where everything is:

map showing all the rooms and things

I just wandered to the chests and checked to see that they had a health, a speed, and a catPersuasion. If you look carefully, you’ll see five chests in there. The other two are Mimics, and I woke both of them up. I ran away quickly, so they didn’t get me.

This test tells me that what we have here will work. But I am unquestionably tired, and my wife has brought home lunch from Wendy’s, so let’s sum up.

Summary

I noticed, because I was looking for it, that I got the same level 1 every time, because we had set randomseed. I improved Main to do randomseed(os.time()), which is the approved priming scheme.

Commit: use randomseed(os.time()) in main.

Also commit: placeDecorXXX shows creation of specific decor.

This morning’s work has provided a handful of functions, all global at this point, that bridge the gap from a fairly decent description of Decor and Loot to an actual dungeon layout of the same. The same scheme, or nearly the same, would allow us to specify “pain”. We might add another array to the zip, or we might fold the pains into the already zipped array. I’m not sure which will be better.

The functions, aside from being naked global functions, are actually pretty decent, I think, and our next step should probably be to pull them together into some kind of object, probably a DecorFactory of some kind.

All in all, a smooth morning, free of confusion, and ending in a delicious Spicy Chicken Sandwich – with added pickle. Highly recommended.

See you next time!