Still working on better control over creation of Dungeon Objects. I think I’ll do a Decor factory method this morning. I can’t think how to test it. The Internet helps me.

On another note, why is it that when I start eating my Morning Banananan … I almost invariably have a fit of sneezing? But I digress …

The path I’m on, over in the SpikeTableZipper, is to create arrays of little control tables, containing key-value pairs, each pair representing one of the parameters of the instance we’re about to create. So my plan for the morning is to code that up.

The question in my mind is how to test it. I think the writing of it is easy and unlikely to be wrong. But the test? Yucch.

Let’s look around.

Here’s Decor’s init. We don’t care about the details, just the parameters. My plan is to build the factory method or methods to deal with these three parameters, and then later to work the “pain” parameter in. I’m doing this for two reasons.

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

First, I want to have the real experience of creating the object from this tabular input, to see how much or little I like it, and, second, there’s a fair amount more to do on the array-building stuff, and I want to make a little progress inside the game. Maybe that’s only one reason. Maybe it’s ten. Maybe I’m really doing this because the banananana… made me sneeze.

Point is, I’m gonna do it. And I really don’t know how to reasonably test it.

Some new Twitter friends are giving me some ideas. First, from Alex I believe, a factory method accepting an approximation to our parameter table:

        _:test("Create from definition table", function()
            local def = {tile="tile", item="item", kind="kind"}
            local decor = Decor:fromDef(def)
            _:expect(decor.tile).is("tile")
            _:expect(decor.item).is("item")
            _:expect(decor.kind).is("kind")
        end)

Since Decor creation considers all its parameters to be pretty much anything, we can just pass in strings. This test demands fromDef:

9: Create from definition table -- Decor:357: attempt to call a nil value (method 'fromDef')

We code:

function Decor:fromDef(defTable)
    return Decor(defTable.tile, defTable.loot, defTable.kind)
end

I expect this to work. I had intended not to complete the method but I just typed it in.

Well, so much for me because it didn’t work at all. This is why we test.

9: Create from definition table  -- Actual: nil, Expected: tile
9: Create from definition table  -- Actual: nil, Expected: item
9: Create from definition table  -- Actual: Skeleton2, Expected: kind

OK, let’s see. First of all, Decor doesn’t save its tile, so whatever we put in there isn’t going to show up. ‘loot’ and ‘ ‘kind’ should have worked, it seems to me. Well, the test doesn’t agree with the code:

        _:test("Create from definition table", function()
            local def = {tile="tile", item="item", kind="kind"}
            local decor = Decor:fromDef(def)
            _:expect(decor.tile).is("tile")
            _:expect(decor.item).is("item")
            _:expect(decor.kind).is("kind")
        end)

function Decor:fromDef(defTable)
    print("fromDef",defTable.tile, defTable.loot, defTable.kind)
    return Decor(defTable.tile, defTable.loot, defTable.kind)
end

The code is right. Also since “kind” is not known, it gets overridden to “Skeleton2”. Let’s instead give that a legit value.

        _: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")
        end)

I expect this to work. I’ll worry about Tile in a moment. It does work. Do I have a test for tile anywhere to learn from?

Yes, we’re using the RecordingEventBus to make sure the moves happen, in this test:

        _:test("Decor shows Loot only once.", function()
            local tile = "tile"
            local item = "item"
            local decor = Decor(tile,item)
            decor.danger = decor.doNothing
            _:expect(Bus:count("moveObjectToTile"), "Decor not added").is(1)
            decor:actionWith()
            _:expect(Bus:count("moveObjectToMe"), "Did not show loot").is(1)
            decor:actionWith()
            _:expect(Bus:count("moveObjectToMe"), "Showed loot twice").is(1)
        end)

I see what threw me off on the word “item” by the way, I’ve been using that all over the tests. Should improve those names for consistency and to avoid future mistakes.

The RecordingEventBus doesn’t keep parameters. Let’s enhance it to do that. We’ll just keep the last ones that came in:

function RecordingEventBus:publish(event, ...)
    self:tick(event, ...)
    return EventBus.publish(self, event, ...)
end

function RecordingEventBus:query(event, ...)
    self:tick(event, ...)
    return EventBus.query(self, event, ...)
end

function RecordingEventBus:tick(event, ...)
    self.lastParameters = {...}
    self.counts[event] = self:count(event) + 1
end

And now we can extend our test, first to check the call.

        _: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(2)
        end)

I put in the “2” there to see it fail:

9: Create from definition table  -- Actual: 1, Expected: 2

Now change that to 1 and check the parameters.

        _: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 runs. Commit: Decor can create from table definition. Extend RecordingEventBus to save last parameters.

I imagine that you might have done two commits. That is not our practice here chez Ron, and so far it has been OK. I can imagine in a real team project we’d keep things like that separate. I also imagine that we’d always be moving forward, so it wold only be the rare revert where this would bite us.

Now we have just one more thing to do. We actually plan to have an array of these tables that define a bunch of Decor items. As it happens, we don’t even expect a return from the creation method in actual use, it’ll just be create the tables and dump let the Decor:fromDef create them. They go into the dungeon and we have no real need to hang on to them.

Anyway, let’s write a test, because now I think it may not be too difficult to get a decent check. I’ll start with this:

        _: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)
        end)

Test should fail for lack of the from method. Do I hate that name?

10: Create from array of definition table -- Decor:373: attempt to call a nil value (method 'fromArrayOfDefinitions')

Code it:

function Decor:fromArrayOfDefinitions(array)
    local f = function(def)
        return Decor:fromDef(def)
    end
    return ar.map(array, f)
end

I think it was Joe, Garth, and Barney who all came up with the idea of using one’s library array stuff to help. I think this will actually work. It does.

I wonder why I can’t just say this:

return ar.map(array, Decor.fromDef)

Ah. Because we call Decor:fromDef, so the first parameter to fromDef is really Decor because Lua. I see no easy way around it and the above is pretty much how we do things with ar.map anyway.

Let’s beef up the test a bit:

        _: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].kind).is("BarrelEmpty")
            _:expect(decors[2].kind).is("BarrelFull")
        end)

We make sure that the tile move was called twice, mostly to match the previous test, and we check the kinds of the two Decor coming back. Should we make sure they are Decor? Sure, why not:

        _: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)

Test passes. Commit: Can create multiple Decor with fromArrayOfDefinitions factory method.

This is enough for a Saturday, let’s sum up.

Summary

This was a new form of mob programming, where you give Twitter followers some vague and slightly inaccurate information, and they offer good ideas and questions, and the result is that your job is easier and turns out rather nice. And it was fun.

This is better than rubber-ducking, because the duck doesn’t say anything. I was not planning on doing the separate creation method, I was planning on just calling the basic constructor. The creation method makes things a bit better, I think. And I had intended to write the loop out longhand, because my habits do not cause me to use my limited “fp” functions as often as I might.

So the code is quite different from what I intended to write, and is better tested than it might have been, because I was fully prepared to argue that the code was trivial enough not to need testing. Never mind that I actually had the code wrong at one point, if not more than one.

So my thanks to Garth, Thomas, Barney, Joe, Christian, Astrid, Chris, Alex, and Jon (for the compliment). I hope I didn’t miss any of the folks who chimed in. It was fun and actually quite helpful.

There’s probably a big lesson here. Maybe I should be live-streaming my programming so as to get help all the time. Perhaps more to the point, we see that people working together are better than the same people working individually. It speaks to things like team rooms (real or virtual), pair and mob programming, and just every now and then asking someone to come take a look.

On the small lesson side, I have now tried using my little scheme of providing a lightly-structured table of construction parameters, and it seems to hold water. There’s not much checking being done, and maybe there should be, especially if we pretend that these tables will be built up by Level Designers using some other imaginary tool. Or, if we pretend that I’m going to type in some tables and might just spell skelleton wrong.

Fun morning, and a little progress. Thanks all!