Loot today. And a possibly interesting problem.

First, the problem. “Bri” from the Codea forum was offering a bit of advice, and mentioned levels, loot, power-ups and the like. Bri mentioned that some games have fixed level maps, although the population of those levels may change. That reminded me of a problem.

Our maps here are random.

I also believe that the changes to put in the new walls have resulted in more rooms that overlap, making for non-rectangular rooms. That was never supposed to happen, but I have to admit I rather like the effect. But I digress.

The map is guaranteed to be fully connected, since I always carve a route between room i and room i+1. And because the rooms are placed randomly, we get multiple connections automatically. The maps look good to me:

map

But the problem. It’s common in games of this type for the adventurer to enter a level at some beginning point, and to have to travel through most of the dungeon before finding their way to the next level. They may not have to visit every room, but they do need to progress through much of the level. There may even be things to be found or done in that level that will help them get through it.

And at the “end” of the level, there is often a “Boss”, a monster more powerful than the others on that level, who must be defeated before the adventurer can get to the next level at all.

The problem. I’m getting there. In a random dungeon where the game literally does not know what the room layout even is, how do we place the adventurer and the exit (and boss) in suitable places? How can we even be clear what we mean by “suitable”, much less implement it?

Perhaps we just do something simple, such as start the player in one corner, and put the boss/exit in the diagonally opposite corner. For values of “corner”, of course. That might be good enough. On the other hand, if we do that, once you come in, you would know immediately what area of the dungeon to avoid until you’re really ready. That might take the edge off the game.

But if we just let it be random, we could wind up starting the payer and boss in the same room. Or the player in a room with only one exit, which can happen, and that exit leads to the boss.

It’ll be easy enough to avoid starting them in the same room: we do have the original room rectangles saved in a collection that is never used. But the game literally does not know the paths at all.

So. It’s a problem. It’s on my mind. I don’t have an answer, and since this isn’t a real game I don’t really have to answer it. But since my general plan here is to show how we program so that problems that arise can be solved without scrapping the whole program and staring over … I may have to solve this one.

But not today. Today is about …

Loot

The only things the player can find in the dungeon, other than trouble, are keys, and chests containing health power-ups. Chests can always be opened, requiring no key.

Long-term, I have in mind that many good items can just be found lying about. I expect to continue to use chests, some of which will contain something good, and some of which will contain something bad, like a monster.

The task today is to create some more goodies that can be dropped around the dungeon.

Let’s look at how that happens now.

function GameRunner:createLevel(count)
    self:createRandomRooms(count)
    self:connectRooms()
    self:convertEdgesToWalls()
    local r1 = self.rooms[1]
    local rcx,rcy = r1:center()
    local tile = self:getTile(vec2(rcx,rcy))
    self.player = Player(tile,self)
    self.monsters = self:createThings(Monster,9)
    for i,monster in ipairs(self.monsters) do
        monster:startAllTimers()
    end
    self.keys = self:createThings(Key,5)
    self:createThings(Chest,5)
    self.buttons = {}
    table.insert(self.buttons, Button("left",100,200, 64,64, asset.builtin.UI.Blue_Slider_Left))
    table.insert(self.buttons, Button("up",200,250, 64,64, asset.builtin.UI.Blue_Slider_Up))
    table.insert(self.buttons, Button("right",300,200, 64,64, asset.builtin.UI.Blue_Slider_Right))
    table.insert(self.buttons, Button("down",200,150, 64,64, asset.builtin.UI.Blue_Slider_Down))
    self:runCrawl(self.initialCrawl, false)
end

In the middle there, we find:

    self.keys = self:createThings(Key,5)
    self:createThings(Chest,5)

And of course:

function GameRunner:createThings(aClass, n)
    local things = {}
    for i = 1,n or 1 do
        local tile = self:randomRoomTile()
        table.insert(things, aClass(tile,self))
    end
    return things
end

We expect all our “things” to be able to be created on a tile, and to expect the GameRunner as a parameter, so that they can link back to the game as needed.

How will we want this to work in the longer term? We may or may not provide for all the ideas, but it’s good to have them.

Things–maybe we should call them Loot–are probably level-dependent. There should be loot that you only get in the lower levels. (Note to self: this will make game testing messy. Magic way to set level for testing?) So there will be a table of Loot types for each level, or something like that. And given the level we’re creating, we’ll create such items.

We may want to control exactly how many there are of a given Loot type. Maybe there can only be one Energy Burster per level. We may want to control groups: there should be at least as many Keys as there are Chests. Other Loot types, we probably want a range of how many show up. Four to seven Healths sort of thing.

We don’t want to write more and more procedural code like we have now as the levels and Loots proliferate. So we’ll want some kind of tabular representation of what to create.

Finally, there’s this. Right now we have a Key class, a Chest class, and a Health class. Keys get counted (i.e. they are in your pocket), Chests know how to open, and if open, they put a Health onto the tile with them. (And, presently, they can’t contain anything other than Health.) Healths, if you step into their tile, give you a random spurt of health.

The process for that is this:

  • You try to enter the tile
  • Method ‘legalNeighbor calls validateMoveTo`
  • Method validateMoveTo calls attemptedEntranceBy on the new tile
  • attemptedEntranceBy checks each contents item in its contents, creating a TileArbiter for each, and sending moveTo to each
  • TileArbiter has a table entry for each possible pair of entrants and each type of content.
  • The table will typically call the entrant’s method startActionWithXXX, where XXX is the class of the tile contents item, e.g. startActionWithHealth.
  • The entrant (typically player) will translate that message into combat, or, as with Health, into a call to addHealthPoints.

Wow, that’s a lot, isn’t it? And we certainly don’t want to have to proliferate a zillion different table entries for every type of Loot. Instead, we’d like to wind up with some general Loot class, or perhaps just a few of them, and to have the various Loots contain enough information to know what to do.

For example, health is stored in the Payer member variable healthPoints. At present, the Health class sends addHealthPoints to the player, which has a specialized method:

function Player:addHealthPoints(points)
    msg = string.format("+%d Health!!", points)
    local f = function()
        coroutine.yield(msg)
    end
    self.runner:runCrawl(f, false)
    self.healthPoints = math.min(20, self.healthPoints + points)
end

What if we wanted a gem that added to the player’s strength? We could duplicate this code with suitable substitutions, and we could refactor that to remove most of the duplication, but we’d still have to add entries to the TileArbiter table and a new method for adding strength.

What if we wanted an object that doubled strength? What if we wanted on that doubled strength, but only for a limited number of moves?

The possibilities seem endless. And while we could stop here and sort through all those possibilities and devise a nifty keen table structure that would include everything, and probably a nifty keen table editor to fill it in, and an extra cost optional package for the user to build their own magical items … that is not our way.

Our way is to be aware of where we might go, but to build no more than we need right now, always keeping things as clean as we can.

In that spirit, let’s do another item like Health, called Strength, and let’s have it, at first, be discovered outside chests. But let’s observe that as soon as we do it, we’ll start duplicating entries in the TileArbiter table, and let’s take the time to avoid doing that. There’s nothing that says we have to create every possible form of duplication and then remove it. We are allowed to avoid it by refactoring first.

There’s a risk with that: we might choose a poor idea toward which to refactor. Ah, well, such is life.

As I started this paragraph, I had decided to implement an abstract superclass for Health, called Loot, and then to make Strength be another subclass of Loot. But about four words in I though of an alternative.

Let’s make a concrete class called Loot, and let’s have it contain a Health, and later, a Strength. Composition rather than Inheritance. Generally thought to be a good thing.

So what does a Loot look like? Should we TDD it? I don’t know. I don’t even know if it has any behavior.

Let’s try to TDD it. I’ll make a new test tab, though the tabs here are driving me crazy. (N.B. There is a whole test tab set not to run. I should run it and fix whatever breaks. Or delete it.)

Anyway:

-- TestLoot
-- RJ 20211225


function testLoot()
    CodeaUnit.detailed = true
    
    _:describe("Loot", function()
        
        local runner
        local room
        
        _:before(function()
        end)
        
        _:after(function()
        end)
        
        _:test("Create Health Loot", function()
        end)
        
    end)
end

So far this works fine. 😉

Perhaps a more robust test is in order. Let’s first worry about creating a free-standing Loot, and later worry about how to get them specified into Chests.

        _:test("Create Health Loot", function()
            local loot = Loot(tile, Health)
            local player = FakePlayer()
            local arb = TileArbiter(loot, player)
            local m = arb:moveTo()
        end)

I’m really just feeling my way here. This is surely enough to fail. Let’s see.

1: Create Health Loot -- TestLoot:20: attempt to call a nil value (global 'Loot')

Good enough to create a class.

-- Loot
-- RJ 20210125

Loot = class()

function Loot:init()
end

Now the error is:

1: Create Health Loot -- TileArbiter:33: attempt to call a nil value (method 'getTile')

That’s here:

function TileArbiter:refuseMove()
    return self.mover:getTile()
end

We got rather deep in, given that we have no idea what a Loot is in this class. Here’s the moveTo we called, and some of the things it calls:

function TileArbiter:moveTo()
    local entry = self:tableEntry(self.resident,self.mover)
    local action = entry.action
    if action then action(self.mover,self.resident) end
    local result = entry.moveTo(self)
    return result
end

function TileArbiter:tableEntry(resident,mover)
    local rclass = getmetatable(resident)
    local mclass = getmetatable(mover)
    local innerTable = TA_table[rclass] or {}
    local entry = innerTable[mclass] or self:defaultEntry()
    return entry
end

function TileArbiter:defaultEntry()
    return {moveTo=TileArbiter.refuseMove}
end

So the TileArbiter is pretty robust against things it doesn’t understand, defaulting to refuse the move. But we actually do want a table entry for Loot. Here’s table creation:

function TileArbiter:createTable()
    -- table is [resident][mover]
    if TA_table then return end
    local t = {}
    t[Chest] = {}
    t[Chest][Monster] = {moveTo=TileArbiter.refuseMove}
    t[Chest][Player] = {moveTo=TileArbiter.refuseMove, action=Player.startActionWithChest}
    t[Key] = {}
    t[Key][Monster] = {moveTo=TileArbiter.acceptMove}
    t[Key][Player] = {moveTo=TileArbiter.acceptMove, action=Player.startActionWithKey}
    t[Player] = {}
    t[Player][Monster] = {moveTo=TileArbiter.refuseMove, action=Monster.startActionWithPlayer}
    t[Monster]={}
    t[Monster][Monster] = {moveTo=TileArbiter.refuseMoveIfResidentAlive, action=Monster.startActionWithMonster}
    t[Monster][Player] = {moveTo=TileArbiter.refuseMoveIfResidentAlive, action=Player.startActionWithMonster}
    t[Health] = {}
    t[Health][Player] = {moveTo=TileArbiter.refuseMove, action=Player.startActionWithHealth}
    TA_table = t
end

We’ll need an entry for Loot here. And recall that when the move is accepted, it’s the mover who gets the message.

    t[Loot] = {}
    t[Loot][Player] = {moveTo=TileArbiter.acceptMove, action=Player.startActionWithLoot}

Now at this moment, I expect to fail with no method startActionWithLoot on my FakePlayer class.

I am fated to be disappointed:

1: Create Health Loot -- TileArbiter:33: attempt to call a nil value (method 'getTile')

That seems odd. It is as if we got the default again.

Let’s test the table entry directly. No, wait. This won’t work, because there’s no entry for FakePlayer.

    t[Loot][FakePlayer] = {moveTo=TileArbiter.acceptMove, action=Player.startActionWithLoot}

Now what happens?

1: Create Health Loot -- TileArbiter:17: attempt to call a nil value (method 'getTile')

Now the move is accepted. But our FakePlayer can’t provide a tile. And what’s going to be done with it anyway? Wait, the table entry for FakePlayer is wrong, though I think we should have failed anyway. Maybe not tho. Ah, right:

function TileArbiter:moveTo()
    local entry = self:tableEntry(self.resident,self.mover)
    local action = entry.action
    if action then action(self.mover,self.resident) end
    local result = entry.moveTo(self)
    return result
end

If we don’t find the method, we just ignore it. So we need to put the method into FakePlayer.

function FakePlayer:startActionWithLoot()
    assert(false,"Loot!")
end

Now I expect the assert.

1: Create Health Loot -- TestLoot:36: Loot!

Does it seem to you that I have no idea what I’m doing, and just fumbling in the dark? I can imagine that it does, but that’s not quite what’s going on.

I’m allowing the results of my test to drive what I do next. I have a general understanding of the flow of the system: I described it above. But I have neither the desire nor the need to keep it all in my head. I’m confident that if I eep enhancing these objects bit by bit, I’ll reach a point where my Loot class can do what I want. So I’m not really fumbling in the dark, I’m following my nose, with my eyes open.

At least that’s my story.

Now we need to get past the assert. One thing for sure, we should be being passed the Loot that we created in the test. Let’s make it accessible and check it in FakePlayer.

local loot

function testLoot()
...
        _:test("Create Health Loot", function()
            loot = Loot(tile, Health)

function FakePlayer:startActionWithLoot(aLoot)
    assert(aLoot == loot)
end

I expect now to fail on the getTile, and glory be, I do:

1: Create Health Loot -- TileArbiter:17: attempt to call a nil value (method 'getTile')

I wonder if I could put an expect` in the FakePlayer. I think not, but let’s try it.

function FakePlayer:startActionWithLoot(aLoot)
    _:expect(aLoot).is(loot)
end

Same error. That’s interesting. I’ve not done much with fake objects in my Codea tests, but if they can contain asserts, that’s rather nice. Now for getTile, what if we were to return a nil?

function FakePlayer:getTile()
    return nil
end

No, that’s wrong. acceptMove asks the resident (our Loot) for its tile. Therefore:

function Loot:init(tile)
    self.tile = tile
    if tile then tile:addContents(self) end
end

This is a bit speculative, but I didn’t want to forget it. If we are given a tile on creation, we’re supposed to set ourselves into its contents. Now this should run OK but I’m not satisfied with the test yet.

Oh yeah, maybe implement getTile, fool. (See what I mean about letting the test lead me? But when I think I know what’s next, it does jolt me. That’s OK too.

function Loot:getTile()
    return self.tile
end

Now the test runs, but as I say, I’m not satisfied with it.

        _:test("Create Health Loot", function()
            loot = Loot(tile, Health)
            local player = FakePlayer()
            local arb = TileArbiter(loot, player)
            local m = arb:moveTo()
        end)

We aren’t doing much expect here, and we can do more. Right now, though, m will be nil. We can set it to anything: no one is using it.

        _:test("Create Health Loot", function()
            local tile = FakeTile()
            loot = Loot(tile, Health)
            local player = FakePlayer()
            local arb = TileArbiter(loot, player)
            local m = arb:moveTo()
            _:expect(m).is(tile)
        end)
        
    end)
end

FakeTile = class()

function FakeTile:init()
end

function FakeTile:addContents(ignored)
end

Test runs. So now we know that a Loot will call the startActionWithLoot on a Player, and we’re confident that it’ll return its tile if it has one. Now we want the Loot to contain some particular kind of benefit, in some sense of contain.

Above, I was thinking to put a Health into the Loot. But would it be even better if the Loot just knew what method to call on the Player? That might be OK, and it’s even simpler than composing in a new object every time. So let’s first make the Loot act like a health.

What should the Player do in her startActionWithLoot? With a Health, she does this:

function Player:startActionWithHealth(aHealth)
    self:addHealthPoints(aHealth:giveHealthPoints())
end

I think here she needs to call the loot (double dispatch, check it out, Chet!) and have it know what to tell her. This should hold us at least for a while.

So, our FakePlayer should be like this:

function FakePlayer:startActionWithLoot(aLoot)
    _:expect(aLoot).is(loot)
    aLoot:actionWith(self)
    _:expect(self.strengthPoints).isnt(nil)
end

So I’m imagining that we’ll configure this Loot to give strength. Let’s see how we could do that. One way would be to have it call addStrengthPoints, but that way lies method proliferation. Let’s instead have it call addPoints(anAttribute, points).

function Loot:actionWith(aPlayer)
    aPlayer:addPoints("Health", 4)
end

Now I can actually make the test tighter, for now:

    _:expect(self.strengthPoints).is(4)

Of course this will fail. I expect it to fail not knowing addPoints:

1: Create Health Loot -- Loot:16: attempt to call a nil value (method 'addPoints')

Now we need a fairly robust addPoints in the real Player, but here we can settle for:

Oh, wait, I should have said “Strength” in the method.

function Loot:actionWith(aPlayer)
    aPlayer:addPoints("Strength", 4)
end

Still need addPoints:

1: Create Health Loot -- Loot:16: attempt to call a nil value (method 'addPoints')
function FakePlayer:addPoints(kind, amount)
    _:expect(kind).is("Strength")
    _:expect(amount).is(4)
end

function FakePlayer:startActionWithLoot(aLoot)
    _:expect(aLoot).is(loot)
    aLoot:actionWith(self)
end

I moved the expect on amount up. This test is not robust, however, because a real Loot is surely going to be random.

Maybe we can make that work though. How about this:

        _:test("Create Health Loot", function()
            local tile = FakeTile()
            loot = Loot(tile, "Strength", 4,4)
            local player = FakePlayer()
            local arb = TileArbiter(loot, player)
            local m = arb:moveTo()
            _:expect(m).is(tile)
        end)

Now we’re telling the Loot what attribute to try to set, and the random range to use. (I should use 5 and change the test. Done.)

1: Create Health Loot  -- Actual: 4, Expected: 5

Can you feel how I just change the test to be the next capability I want, and it leads me to do the implementation? Now I enhance Loot:

function Loot:init(tile, kind, min, max)
    self.tile = tile
    self.kind = kind
    self.min = min
    self.max = max
    if tile then tile:addContents(self) end
end

Save the parms. And …

function Loot:actionWith(aPlayer)
    aPlayer:addPoints(self.kind, math.random(min,max))
end

Use them. I expect a pass.

1: Create Health Loot -- Loot:19: bad argument #1 to 'random' (number expected, got nil)

How about typing self in there? Standard error #something.

function Loot:actionWith(aPlayer)
    aPlayer:addPoints(self.kind, math.random(self.min, self.max))
end

See how when I make a dumb mistake, the test tells me? That’s exactly how we want it to go when we TDD.

NOW, dammit, I expect the test to pass.

1: Create Health Loot  -- OK

Yes!

Now loot is robust enough that we could use it. But if it’s lying around, it needs to be able to draw itself. And it should remove itself from its tile if it’s used, I imagine. How could we test that last bit? We can implement removeContents on our FakeTile, and expect it to be called. That could be done like this:

function FakeTile:removeContents(anEntity)
    removedEntity = anEntity
end

Now we can add that local and check it:

        _:test("Create Health Loot", function()
            local tile = FakeTile()
            loot = Loot(tile, "Strength", 5,5)
            local player = FakePlayer()
            local arb = TileArbiter(loot, player)
            local m = arb:moveTo()
            _:expect(m).is(tile)
            _:expect(removedEntity).is(loot)
        end)

Hmm, didn’t work:

1: Create Health Loot  -- Actual: nil, Expected: table: 0x292fb3400

Did we not get called, or did we get called wrongly?

function FakeTile:removeContents(anEntity)
    _:expect(anEntity).is(loot)
    removedEntity = anEntity
end

We may need to set another flag, but let’s see what this does. Looks like it’s not called. Oh, did we implement the feature? Well, no.

function Loot:actionWith(aPlayer)
    aPlayer:addPoints(self.kind, math.random(self.min, self.max))
end

The tests drive but sometimes they have to shout pretty loudly to get my attention. We’ll do the thing:

function Loot:actionWith(aPlayer)
    aPlayer:addPoints(self.kind, math.random(self.min, self.max))
    self.tile:removeContents(self)
end

And the test runs. Now, as I was saying, these things have to know how to draw. But first, let’s commit this: Loot internals working.

Let’s also pause to reflect. And decide after that whether we’re done for the morning. It’s about 1025, and I probably started right around 0800 or earlier. It has been a smooth morning, though, and my chai’s not quite gone. A bit weak, though, the ice has been melting.

Reflection

I honestly thought TDDing the Loot object would be a waste of time. At first, I didn’t even see how to get a handle on it. But it seemed right to at least try, and it has turned out to be really good.

I used a FakeRandom object in one set of tests, but this is the first time I’ve used fake objects that emulate real objects in the system. In general, using fake object is more “London School” of TDD, rather than “Detroit School”. I am one of the first “Detroit School” folks, so I use fakes–or any kind of test double object–very rarely.1

These fakes have turned out to be very helpful, since we’re interested in the message flow among our various existing, and soon-to-be-existing objects. And that is a common case for test doubles. However, for me, they always leave me with a bit of uncertainty. For example, our FakePlayer now has a method that the real Player does not have, namely startActionWithLoot. In fact it has two! I forgot addPoints.

There’s probably a well-known way, in London School, of ensuring that the real object implements the methods we’ve discovered on our test double, but here in my world, I have to remember to do it. Which is a flawed notion, since trusting me to remember stuff is … what were we talking about?

Be that as it may, we need to put those methods into Player, so let’s do it now:

function Player:startActionWithLoot(aLoot)
    aLoot:actionWith(self)
end

Now we know the Loot will call addPoints. We need to be prepared for more than one kind but for now, let’s assume only one:

function Player:addPoints(kind, amount)
    local attr = self:pointsTable(kind)
    if attr then
        local current = self[attr]
        self[attr] = math.min(20,current + points)
        self:doCrawl(kind, amount)
    end
end

function doCrawl(kind, amount)
    local msg = string.format("+%d "..kind.."!!", amount)
    local f = function()
        coroutine.yield(msg)
    end
    self.runner:runCrawl(f, false)
end

I think this is good. Needs to be tested in game, however. For that, we need to deal with making these babies visible. That will involve giving them at least a draw function, and then we’ll need to spray them around somehow.

Loots probably draw themselves a lot like chests or healths. Here’s Health:

function Health:draw(tiny)
    if tiny then return end
    pushStyle()
    spriteMode(CENTER)
    local g = self.tile:graphicCenter()
    sprite(asset.builtin.Planet_Cute.Heart,g.x,g.y+10, 35,60)
    popStyle()
end

We’ll adapt that:

function Loot:draw()
    if tiny then return end
    pushStyle()
    spriteMode(CENTER)
    local g = self.tile:graphicCenter()
    sprite(asset.builtin.Planet_Cute.Gem_Blue,g.x,g.y+10,35,60)
    popStyle()
end

Now, how can I spray these around? I don’t think I can use my thing logic, as it doesn’t have enough parameters. Let’s just do a new loot creation function in GameRunner.

function GameRunner:createLoots(n)
    local loots = {}
    for i =  1, n or 1 do
        local tile = self:randomRoomTile()
        table.insert(loots, Loot(tile, "Strength", 4, 9))
    end
    return loots
end

Now I’ll create a lot of them for now, 10. And run.

Player:212: attempt to call a nil value (method 'pointsTable')
stack traceback:
	Player:212: in method 'addPoints'
	Loot:19: in method 'actionWith'
	Player:235: in local 'action'
	TileArbiter:27: in method 'moveTo'
	Tile:88: in method 'attemptedEntranceBy'
	Tile:348: in function <Tile:346>
	(...tail calls...)
	Player:150: in method 'moveBy'
	Player:143: in method 'keyPress'
	GameRunner:213: in method 'keyPress'
	Main:34: in function 'keyboard'

Oh yes. Just as the tests drive me, so does this manual testing, but it’s far less likely to be helpful, because I can’t be relied on to test the right thing.

function Player:pointsTable(kind)
    local t = {Strength=strengthPoints}
    return t[kind]
end

I think this works. I’ll double check that strengthPoints is the player’s strength attribute. And it is. Now run.

Well, that’s interesting. I can run over the gems and they vanish, but I don’t get any strength messages. Maybe that table format doesn’t work the way I think it does.

Let’s test that. No, wait. That code can’t work as written. We need to return a string, to use in the other method. Revising:

function Player:pointsTable(kind)
    local t = {Strength="strengthPoints"}
    return t[kind]
end

I rather expect this to work now.

Player:215: attempt to perform arithmetic on a nil value (global 'points')
stack traceback:
	Player:215: in method 'addPoints'
	Loot:19: in method 'actionWith'
	Player:240: in local 'action'
	TileArbiter:27: in method 'moveTo'
	Tile:88: in method 'attemptedEntranceBy'
	Tile:348: in function <Tile:346>
	(...tail calls...)
	Player:150: in method 'moveBy'
	Player:143: in method 'keyPress'
	GameRunner:213: in method 'keyPress'
	Main:34: in function 'keyboard'

Well, yes. We must think about going back to TDD on this. But for now, debug it.

function Player:addPoints(kind, amount)
    local attr = self:pointsTable(kind)
    if attr then
        local current = self[attr]
        self[attr] = math.min(20,current + amount)
        self:doCrawl(kind, amount)
    end
end

I was adding points before.

Player:216: attempt to call a nil value (method 'doCrawl')
stack traceback:
	Player:216: in method 'addPoints'
	Loot:19: in method 'actionWith'
	Player:240: in local 'action'
	TileArbiter:27: in method 'moveTo'
	Tile:88: in method 'attemptedEntranceBy'
	Tile:348: in function <Tile:346>
	(...tail calls...)
	Player:150: in method 'moveBy'
	Player:143: in method 'keyPress'
	GameRunner:213: in method 'keyPress'
	Main:34: in function 'keyboard'

Standard error # something. Failed to put class name on method:

function Player:doCrawl(kind, amount)
    local msg = string.format("+%d "..kind.."!!", amount)
    local f = function()
        coroutine.yield(msg)
    end
    self.runner:runCrawl(f, false)
end

And it works:

gem

crawl

Commit: blue gems are strength-providing Loot.

Definitely time to stop for the morning, it’s 1112.

Let’s sum up.

Summary

We wanted to have new kinds of loot. We considered a few designs:

  • Subclassing from a Loot superclass
  • A Loot class holding another value-bearing object
  • A Loot class that knows what attribute to add to.

We chose the last option, essentially on the fly, as it came to us during TDD that we didn’t need an object inside, only the public (“Strength”) and private (strengthPoints) names of the attribute. This won’t deal with harmful objects, or spells or scrolls, but we do what we need, not what we might need.

So for now, it’s sufficient, and that’s how we like it.

Overall, the design seems solid, and I’ll convert Health hearts to use Loot, probably next time. It will be interesting to make Chests into a kind of Loot, as their interaction is different. But we’ll burn that bridge when we come to it.

Generally, it has gone smoothly, and TDD helped a lot with that. The test double Fake objects were new to us, and they worked out pretty well. I remain unsure how to more the necessary functions from the FakePlayer into Player and so on, but that was fairly straightforward. Maybe some London school person will message me with some guidance.

When I moved away from TDD to on-screen testing, I ran into more surprises. However, setting up the objects to work in TDD tests seemed too much, and seemed unnecessary. I wasn’t necessary, but given how smoothly we went with TDD, and how much rougher it got without, it seems that it might have been valuable. Was it too much work to do We’ll never know, unless we go back and try to write the tests now.

And we’re not likely to do that. This often happens. There’s a potentially valuable lesson to be learned here, and I’m going to leave it on the table, because I’m too tired, or too lazy, to do the work.

I forgive myself for that. I’ve given what I’ve got, and that’s always good enough. I’d forgive you, too, were you to need it. But you don’t need my permission nor forgiveness for your decisions. Those are up to you.

See you next time, I hope!


D2.zip

  1. Some folks refer to “Chicago School”. This is a misnomer. I’m not sure whether this came about because people didn’t know the difference between Detroit and Chicago, or whether Chicago players tried to usurp the name. Trust me, I was there at the beginning, and “Detroit School” is the pure quill.