We’re still prepping for the story about better control over what goes into the Dungeon. Some things remain that I’d like to clear up. And a small story!

In the past few days, I spent some time cleaning up and standardizing calls to getTile, including, finally, making the calls polymorphic, accepting either a position (vec2) or x and y coordinates. In most of the cases, I’ve passed in whichever parameter type the using code found most convenient. Previously, depending on where you were standing, you might have to coerce to a position when you had x and y, or coerce to x and y when you had a position. Somehow I managed to make it inconvenient for both sides.

There is another implementation of getTile, which is used like this:

function GameRunner:mapToWayDown()
    return PathMap(self:getDungeon():asTileFinder(), self.wayDown:getTile())
end

function GameRunner:playerDirection(aTile)
    return aTile:directionTo(self.player:getTile())
end

function DungeonTestPlayer:getTile()
    return self.dungeon:getTile(self.pos)
end

function DungeonObject:getTile()
    return DungeonContents:tileContaining(self)
end

function Entity:manhattanDistanceFromTile(tile)
    return self:getTile():manhattanDistance(tile)
end

function Entity:remove()
    self:getTile():removeContents(self)
end

Well, you get the idea. The method is implemented on DungeonObject and can therefore be sent to anything in the dungeon, whether a treasure, a trap, a monster, or the Princess. Because of the other meaning, which actually gets a tile given its coordinates, I’d like to rename this one to something better. I am at somewhat of a loss as to what a better name might be.

How about currentTile? currentTileUponWhichYourGoodSelfResides? I think we’ll go with currentTile. This should be nothing but a global rename, except that I don’t trust Codea’s global rename. Maybe I should try it. I’m on a fresh commit.

Sure, I enjoy seeing things blow up. By Jove it seems to have worked. Tests run, game runs. No occurrences of “getTile()” left in the program. Neat. Commit: rename getTile() to currentTile().

Nice. Let’s get to that story.

A “Big” Story

The story is, according to a yellow sticky note on my desk:

Better control over placing DungeonObjects. Counts, etc.

I call that big because, first of all, we have no clear indication of what it means, and, second, even offhand I can guess that we’ll want to choose, for each level, which treasures can be found, how many of each, and where they might be found, that is, just lying about, hidden inside Decor, or in Chests.

Further, I happen to know that Chests are not fully implemented: they always contain a Health potion. Surely we’d like them to contain different items. Besides, why aren’t Chests just a kind of Decor?

And, if that weren’t enough, while most of the items you can receive are instances of Loot class, Keys are a separate unique class.

Finally, we will probably want some sort of textual format for specifying each level’s contents, so that our Level Design Department can decide what they want, fill out a form, and the system munches the data and does what it’s told.

Idea: Invisible Decor

I have an idea, and I’ve had it before, so let’s write it down. Much of what is found in the dungeon is Loot inside Decor (those barrels, boxes, and skeletons that we see lying about). But some Loot is just lying there on the ground. (Same is true of Keys).

One possible notion is just to say that all Loot will be inside a Decor, and stop leaving valuables lying about. But there is a bit of charm to just discovering something lying there. It seems to me that we could create a new kind of Decor, an InvisibleDecor if you will, that contains a Loot. If the Level Design Division so decides, they can specify some number of InvisibleDecor, containing whatever Loots they would like to leave lying about.

End Idea

It’s pretty clear to me that I can’t do all that this morning, and therefore this is a big story and it needs to be split into smaller stories. Ideally, each of these would provide at least a little value to the game and to my product definition person or persons (me, with other hats on).

I’m going to try brainstorming some stories. This will be tricky, as there’s just me and the cat here, and the cat is not as good in a brainstorming session as one might want. But I’ll list some things that may need to be done, and try to cast them as small stories adding up to the larger idea.

Small Stories

Some of these may seem to have no value to the customer. I’m listing them because I think they’ll need to be done, and either we’ll do them as part of a value-bearing story, or we’ll figure out why they have value. Here goes:

  • Make Key a kind of Loot/InventoryItem.
  • Make Chest a kind of Decor (if possible).
  • Create InvisibleDecor, use for “free range” Loot.
  • Allow designer to specify how many of each Decor are deployed, and what they contain (randomly or exactly).
  • Provide easy programmatic interface for contents control, so that programmers can transcribe design requirements.
  • Provide textual format to accept design requirement directly.
  • Move Keys from characterSheet to Inventory.

Design Review

These ideas are based on my memory of how the system works, and while I’ve certainly glanced at the relevant classes recently, I’ve not looked at them with an eye to doing anything like this with them.

So we’ll spend a Quick Design Review reminding ourselves how things work now.

We specify DungeonContents like this:

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

I wonder in passing whether Spikes should be folded in to this exercise. On the one hand, surely there will be the need to say how many appear. But it’s not much the same as boxes with loot in them. So we are dealing with three methods that seem similar as we look at them here, placeLoots, createDecor, and createThings.

Clearly these ideas are conceptually similar. Is their code similar, or even better, somehow converged down to a smaller notion? I doubt it very much. I wasn’t that clever or careful.

While we’re here, let’s rename the “create” ones to “place”: Global Replace, since it works. Commit: rename createThings and createDecor to place.

Now, let’s drill down a bit, starting here:

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

function DungeonBuilder:placeDecor(n)
    local sourceItems = {
        InventoryItem("cat_persuasion"),
        --
        InventoryItem("curePoison"),
        InventoryItem("pathfinder"),
        InventoryItem("rock"),
        --
        InventoryItem("nothing"),
        InventoryItem("nothing"),
        InventoryItem("nothing")--]]
    }
    local items = {}
    for i = 1,n or 10 do
        table.insert(items, sourceItems[1 + i%#sourceItems])
    end
    Decor:createRequiredItemsInEmptyTiles(items,self)
end

function DungeonBuilder:placeLoots(n)
    for i =  1, n or 1 do
        local tab = RandomLootInfo[math.random(1,#RandomLootInfo)]
        local tile = self:randomRoomTileAvoidingRoomNumber(self.playerRoomNumber)
        Loot(tile, tab[1], tab[2], tab[3])
    end
end

function DungeonBuilder:placeThings(aClass, n)
    for i = 1,n or 1 do
        local tile = self:randomRoomTileAvoidingRoomNumber(self.playerRoomNumber)
        aClass(tile)
    end
end

We’d better have a look at RandomLootInfo …

local RandomLootInfo = {
    {"strength", 4,9},
    {"health", 4,10},
    {"speed", 2,5 },
    {"pathfinder", 0,0},
    {"curePoison", 0,0}
}

Now a quick review of Loot, and probably Chest and Key. I’m going to show quite a bit of Loot here, though not all of it. We need to try to take it in.

Loot = class(DungeonObject)

local LootIcons = {strength="blue_pack", health="red_vial", speed="green_flask",
pathfinder="blue_jar", curePoison="red_vase"}

local LootDescriptions = {strength="Pack of Steroidal Strength Powder", health="Potent Potion of Health", speed="Potion of Monstrous Speed" }

function Loot:init(tile, kind, min, max)
    self.kind = kind
    self.icon = self:getIcon(self.kind)
    self.min = min
    self.max = max
    self.desc = LootDescriptions[self.kind] or self.kind
    self.message = "I am a valuable "..self.desc
    if tile then tile:moveObject(self) end
end

function Loot:actionWith(aPlayer)
    local item
    self:currentTile():removeContents(self)
    item = self:createInventoryItem(aPlayer)
    item:addToInventory()
end

function Loot:createInventoryItem(aPlayer)
    return InventoryItem(self.kind, math.random(self.min,self.max))
end

There’s an issue here that I noticed yesterday. These objects implement actionWith, and Player sends actionWith to them when they’re encountered. The usual convention is to send startActionWithPlayer, not actionWith. I’m not sure whether to fix that or not, but I’m not going to do it right this second. Make a sticky: actionWith should be startActionWithPlayer.

Anyway, when the player tries to step on a tile containing a loot, the TileArbiter rules are:

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

You can’t step in, and the actionWith will happen and the Loot will be added to your inventory (and removed from the tile it was on). After that, since it’s gone, you can step onto it. Curiously, Decor do allow you to step on them:

    t[Decor][Player] = {moveTo=TileArbiter.acceptMove, action=Player.startActionWithDecor}

While we’re here, what about Chest and Key?

    t[Chest][Player] = {moveTo=TileArbiter.refuseMove, action=Player.startActionWithChest}
    t[Key][Player] = {moveTo=TileArbiter.acceptMove, action=Player.startActionWithKey}

There’s nothing like consistency, is there? In the case of the Chest, it makes sense not to allow you to step on it, because the Chest is going to open and display whatever is inside. Once the Chest is open, if you attempt again to step on the tile, you’ll be refused, but the new item will be given to you. However, if I recall correctly, that’s rather ad hoc at present:

function Player:startActionWithChest(aChest)
    aChest:open()
end

function Chest:open()
    if self:isOpen() then return end
    self.opened = true
    self.pic = self.openPic
    return Loot(self:currentTile(), "health", 4,10) -- returned for testing
end

It’s not obvious, but the creation of the Loot causes it to be placed on the tile and therefore to be displayed. And it’s pretty clear that there is only one possibility, a random Health.

It should be possible to converge Chest and Decor, but since Chest has two states and Decor is always open, I think we should leave them separate.

Here’s a story we can do today:

Extend Chest to allow it to contain any kind of Loot. Fill Chests with some random loot, not just Health.

Random Loot in Chests

For our first implementation we’ll select random Loot just as we do in Decor:

I think I’ll break out creation of Chests from the createThings method.

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

I can implement that and commit if I do this:

function DungeonBuilder:placeChests(numberToCreate)
    self:placeThings(Chest,numberToCreate)
end

Test. Green. Commit: new placeChests method provides place to stand for Chests with random loot.

Now, since Chest is like Decor, we’ll crib from this:

function DungeonBuilder:placeDecor(n)
    local sourceItems = {
        InventoryItem("cat_persuasion"),
        --
        InventoryItem("curePoison"),
        InventoryItem("pathfinder"),
        InventoryItem("rock"),
        --
        InventoryItem("nothing"),
        InventoryItem("nothing"),
        InventoryItem("nothing")--]]
    }
    local items = {}
    for i = 1,n or 10 do
        table.insert(items, sourceItems[1 + i%#sourceItems])
    end
    Decor:createRequiredItemsInEmptyTiles(items,self)
end

I’m tempted to start here and push downward, but that could leave me without a safe commit for a while. Let’s modify Chest to accept an arbitrary Loot. I’ll review how Decor handles this.

function Decor:init(tile, item, 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.item = item
    -- self.tile = nil -- tile needed for TileArbiter and move interaction
    tile:moveObject(self)
    self.scaleX = ScaleX[math.random(1,2)]
    local dt = {self.doNothing, self.doNothing, self.castLethargy, self.castWeakness}
    self.danger = dt[math.random(1,#dt)]
end

function Decor:actionWith(aPlayer)
    local round = CombatRound(self,aPlayer)
    self.danger(self,round)
    self:giveItem()
end

For now, Chests will not be dangerous (unless they happen to be a Mimic). Let’s change the way they work, so that rather than display the Loot and add it to the tile, they immediately give it to the Player. How does Decor do that?

function Decor:giveItem()
    if self.item then
        self.item:addToInventory()
        self.item = nil
    end
end

I guess that the addToInventory issues the message about “you have received”. We’ll not check that code, but we’ll want to be sure that the message does come out.

Now a subtlety finally comes clear to me. The Decor contain instances of InventoryItem, not Loot. Loot has a tile and perhaps other behavior. InventoryItems never appear in the dungeon directly. So we want Chest to create an inventory item. And there is apparently some test that expects it to return a loot.

We’ll burn that bridge when we come to it.

First I modify Chest like this:

function Chest:open()
    if self:isOpen() then return end
    self.opened = true
    self.pic = self.openPic
    InventoryItem("health"):addToInventory()
end

That does break a test but the in-game effect is that the chest opens and the “you have received a potent potion of health” message comes out. The chest shows open and empty, as we expected.

Check the test. I think we can remove it.

5: Chest-Loot Drawing Order  -- Actual: 1, Expected: 2
        _:test("Chest-Loot Drawing Order", function()
            local ord, entry, tile, loot
            tile = dungeon:getTile(vec2(9,9))
            local chest = Chest(tile)
            DungeonContents:moveObjectToTile(tile)
            ord = DungeonContents:drawingOrder()
            entry = ord[tile]
            _:expect(#entry).is(1)
            _:expect(entry[1]).is(chest)
            loot = chest:open()
            ord = DungeonContents:drawingOrder()
            entry = ord[tile]
            _:expect(#entry).is(2)
            _:expect(entry[1]).is(chest)
            _:expect(entry[2]).is(loot)
        end)

Right. I would like to remove this test. It’s testing drawing order of the Loot, so that it draws on top of the Chest. That’s no longer applicable. But I bet we have no other tests for Chest. Make a sticky: Does Chest need [more] tests? Remove this one anyway.

We can commit, so we shall: Chest now automatically gives the Health that it “contains”.

For our next tiny step, we’ll give the Chest an item member variable, initialize it durning init, and use it in open:

function Chest:init(tile, item)
    tile:moveObject(self)
    self.closedPic = "mhide01"
    self.openPic = asset.open_chest
    self.pic = self.closedPic
    self.opened = false
    self.item = item or InventoryItem("health")
end

function Chest:open()
    if self:isOpen() then return end
    self.opened = true
    self.pic = self.openPic
    self.item:addToInventory()
end

This should behave as before. Test. Yes. Commit again: move Chest health creation to init. Prepare to allow other items.

Now we can create Chests that contain other precious items, as we do with Decor. So:

function DungeonBuilder:placeChests(numberToCreate)
    local sourceItems = {
        InventoryItem("cat_persuasion"),
        InventoryItem("strength"),
        InventoryItem("health"),
        InventoryItem("nothing")
    }
    for i = 1,numberToCreate do
        local item = sourceItems[1+i%#sourceItems]
        local tile = self:randomRoomTileAvoidingRoomNumber(self.playerRoomNumber)
        Chest(tile,item)
    end
end

Tests run and Chests work as advertised. Commit: Chests now provide an InventoryItem from a local list of possibilities.

We’ve made a bit of progress toward our big story and have a new story implemented:

Chests can now contain arbitrary inventory items (or nothing).

Let’s call it a morning and sum up.

Summary

We’ve refreshed our mind and limbered up our fingers as to how things go into the Dungeon. We’re better equipped to work on our big story about providing better control over what’s in there. And we’ve released a small but notable improvement to the game.

We only committed six times in two hours, or 20 minutes per commit. That’s about my usual outer limit before I start thinking I’m in trouble, but today there was a lot of code review and writing about how things work. There was a 35 minute gap between commits while we studied the code.

So. A good day. Two hours, some general code improvement, some preparation for working on the next big story, and a small step in the general direction we want to be heading.

I call that a win. See you next time!


D2.zip