Some consolidation and refactoring is what I have in mind. What will I really do? At this moment, I have little or no idea.

Yesterday’s summary created quite a list of things that might need improvement. Short form:

  • New inventory item creation involves several edits in different places;
  • Monsters are just one class, but they are developing more and more unique per-type behavior. Suggests a subclass or delegate?
  • Publish-Subscribe nicely reduces direct connections, but it also requires coordination between remote parts of the code so that they use the same event names and such.

Looking at these topics one way makes me think that, at least in a real game application, we’d want more capability in our “Making App”, the app that helps us make the “Shipping App”, the actual game. That might be fun to think about. Let’s use our Rock as an example.

I began, if I recall, by finding a Rock. I was looking for a Brick, but couldn’t find one of those, but I did stumble, as it were, across a rock.

I began by looking at the Decor creation, because I’m trending toward having all discoverable items inside Decor, rather than just lying about as loose Loot. (Trending: Might get there, might not. Anyway, decided to do it inside Decor:)

function GameRunner:createDecor(n)
    local sourceItems = {
        --
        InventoryItem{ icon="red_vase", name="Poison Antidote", object=self.player, method="curePoison" },
        InventoryItem{ icon="blue_jar", name="Magic Jar", object=self.player, method="spawnPathfinder", description="Magic Jar to create a Pathfinding Cloud Creature" },--]]
        InventoryItem{ icon="rock", name="Rock", object=self.player, method="dullSharpness", description="Mysterious Rock of Dullness" },
        --
        InventoryItem{object=self.player},
        InventoryItem{object=self.player},
        InventoryItem{object=self.player},--]]
    }
    local items = {}
    for i = 1,n or 10 do
        table.insert(items, sourceItems[1 + i%#sourceItems])
    end
    Decor:createRequiredItemsInEmptyTiles(items,self)
end

Here we create InventoryItems, which expect an input table telling them what they need to know. Using our Rock as an example, we can imagine an outline for creating the Item like this:

InventoryItem
    name: Rock
    icon: rock
    object: player
    method: dullSharpness

What we’re saying here is that we have an InventoryItem whose name is Rock, whose icon is something (a Sprite) named rock, and that when it is clicked, it sends “dullSharpness” to the player.

“rock” might not be a good name for the Sprite, and perhaps “Rock” isn’t even a good name for the item. But the computer doesn’t care, and for right now, those are the names we used.

But there is no Sprite called “rock”. So I imported that graphic I found as a Sprite:

    sheet = asset.Rock
    names = {"rock"}
    Sprites:add(names,sheet,0,0,0,0)

The above is an entry in Sprites:initializeSprites, which reads in the asset sprite sheets and divides them up if necessary. This one doesn’t need division, which is represented by the fact that there’s only one name in its names table: each slice of a sheet gets its own name. The zeros are ad-doc values to adjust how much of the sprite sheet is to be skipped over when slicing. It amounts to cropping the sheet before slicing.

So how might we extend our outline to allow for the Sprite? I can imagine two ways:

InventoryItem
    name: Rock
    icon: rock
    object: player
    method: dullSharpness
...
Sprite
    name: rock
    sheet: asset.Rock
    crop(0,0,0,0)

Here we have the item and sprite defined separately, but in the same outline. We might also do it this way:

InventoryItem
    name: Rock
    icon: Sprite
        name: rock
        sheet: asset.Rock
        crop(0,0,0,0)
    object: player
    method: dullSharpness

Here, we’ve embedded the Sprite definition inside the InventoryItem’s outline. We could even leave the name out, perhaps auto-generating it as “Rock-sprite” or something. And of course, crop could default to all zeros.

I’m wondering how InventoryItem uses the info it has. Let’s look.

function InventoryItem:init(aTable)
    self.icon = aTable.icon
    self.name = aTable.name or self.icon
    self.object = aTable.object or self
    self.message = aTable.method or "print"
    self.description = aTable.description or self.name
    self.attribute = aTable.attribute
    self.value = aTable.value
end

function InventoryItem:draw(pos)
    fill(136,129,107)
    rect(pos.x,pos.y,64,64)
    sprite(Sprites:sprite(self.icon), pos.x,pos.y, 40)
end

The icon variable is used to look up the sprite, here:

function Sprites:sprite(name)
    if type(name) == "string" then
        local spr = sprites[name] or asset.banner_2
        return spr
    else
        return name
    end
end

According to that code, if you pass in a string, it looks it up in a table called sprites, a local table that it uses to store all the sprites it knows by name:

Sprites = class()

local sprites = {}

But that code also allows a sprite to be passed in that is not a string. Does that happen, and if so, where and why?

function Button:draw()
    pushMatrix()
    pushStyle()
    spriteMode(CENTER)
    translate(self.x,self.y)
    tint(255,255,255,128)
    sprite(Sprites:sprite(self.img), 0,0, self.w, self.h)
    popStyle()
    popMatrix()
end

Here, the img is in fact of type image and has been drawn to be a rectangle containing the button text.

function WayDown:draw(tiny, center)
    sprite(Sprites:sprite(asset.steps_down),center.x,center.y,64,64)
end

Here, the WayDown knows its asset directly, passes it to Sprites, which returns it.

Why are we doing this? We could just as well have said this:

function WayDown:draw(tiny, center)
    sprite(asset.steps_down,center.x,center.y,64,64)
end

The effect is the same.

If I recall–or making up a possible reason now–the idea is to have all the draw functions become more and more alike, so that we can move them upward in the hierarchy, making them less specialized, reducing the need to have so many of them, and making them less error prone.

So I believe that I made the decision that we’d “always” use Sprites, so as to leave the code agnostic to what particular kind of thing we have as our icon, a sprite name, an asset, or (in principle) some other thing that the sprite function might return.

We might never get there. But we are in the business of changing code, and one of the ways we change it is to be more and more similar when we’re doing similar things, so that we’ll understand what we see more readily and so that we can, when the time comes, consolidate similar notions.

But I digress.

Next, we had to implement dullSharpness on Player:

function Player:dullSharpness()
    local msg = "You have dulled all the sharp objects near by."
    Bus:publish("addTextToCrawl", self, {text=msg})
    Bus:publish("dullSharpness")
end

Here, we give a specialized message to the user, saying what happened. We also have the general message “You have used the Mysterious Rock of Dullness”, which was issued by the Inventory.

And then we publish the general message “dullSharpness”. That has to be subscribed to by the appropriate Monsters:

function Monster:initSubscriptions()
    if self:name() == "Ankle Biter" then
        Bus:subscribe(self, self.becomeDull, "dullSharpness")
    end
end

We could imagine other monsters that become dull. Perhaps the Death Fly can’t bite, or the Toothhead, or the Murder Hornet. Or the Vampire Bat or Serpent or Yellow Widow. There’s a lot of bitey stuff up in this um dungeon. And we implemented becomeDull:

function Monster:becomeDull()
    self.damageRange = {0,0}
end

All this does, for now, is reduce the possible damage that the Ankle Biter can do down to zero. He still saws and cuts and grinds, but nothing happens. He’s dull, you see.

So we have to add material, somehow, to our outline, including the actions these guys take, or at least referring to the specific that the programmers have provided. The outline might start to look like this:

InventoryItem
    name: Rock
    icon: Sprite
        name: rock
        sheet: asset.Rock
        crop(0,0,0,0)
    object: player
    method: dullSharpness
        say: You have dulled all the sharp objects near by
        publish: dullSharpness
Monster
    type: Ankle Biter, Murder Hornet
    subscribe:dullSharpness
    do: 
        damageRange = 0,0

The Monster stuff might be right here near the item, or it might be down among other monster stuff. It would be nice, I imagine, if we could keep it distributed like the above, where a given idea could be kept together.

One nice thing about having this outline form of specification might be that it wouldn’t require us to think of and sort out so many method names. You can at least imagine that the various names we use currently to connect things, rock, dullSharpness, becomeDull, and so on, could be either auto-generated, or perhaps these linkages would be implemented in some kind of control table that was “above” the objects.

I’m not sure at this moment that I want to do an outline table like that, and I’m even less sure what form it should really take. It needs some kind of structure, and probably free-form isn’t it, and I hope YAML isn’t it.

I do wonder whether we can simplify what we have here.

Right now, the InventoryItem’s table is this:

        InventoryItem{ icon="rock", name="Rock", object=self.player, method="dullSharpness", description="Mysterious Rock of Dullness" },

Maybe there’s no need to go through the player for this. We operate the item with this code:

function InventoryItem:touched(aTouch, pos)
    if aTouch.state == ENDED and manhattan(aTouch.pos,pos) < ItemWidth//2 then
        Inventory:remove(self)
        self:informObjectRemoved()
        self.object[self.message](self.object, self.attribute, self.value)
    end
end

There we send the message (which is method in the table to the object. Imagine that we had another element in the table, the appliedMessage, and that object was Bus and method was publish and that attribute and value were set such that we just published the dullSharpness` from here instead of through Player.

That would be more direct, and while the table would be a bit more complicated, we’d save pushing random methods down into Player.

Shall we try that?

Let’s do.

I’ll try this:

        InventoryItem{ icon="rock", name="Rock", object=Bus, method="publish", attribute="dullSharpness", description="Mysterious Rock of Dullness" },

I’ll turn off other options in Decor, so that all Decor have rocks, and I need to make the Monsters mostly Ankle Biters.

nearly works

So that’s nearly good. We don’t get any message out when we use the Rock, but we can see the count go down. I expected the “You have used” message, so we need to check that. But the publish worked, because the Ankle Biter scored no more points on us after we touched the Rock.

So this scheme is semi-good. Why didn’t I see that message?

function InventoryItem:touched(aTouch, pos)
    if aTouch.state == ENDED and manhattan(aTouch.pos,pos) < ItemWidth//2 then
        Inventory:remove(self)
        self:informObjectRemoved()
        self.object[self.message](self.object, self.attribute, self.value)
    end
end

function InventoryItem:informObjectRemoved()
    self:informObject("You have used a "..self.description.."!")
end

function InventoryItem:informObject(msg)
    local implementsInform = self.object["inform"]
    if implementsInform then self.object:inform(msg) end
end

Ah. I think we should just send that message directly, rather than through inform. Or, of course, we could implement inform on Bus, and retain the ability NOT to get the message out.

function Player:inform(message)
    if type(message) == "string" then
        message = splitStringToTable(message)
    end
    for i,msg in ipairs(message) do
        Bus:publish("addTextToCrawl", self, {text=msg})
    end
end

That could be useful on Bus. Let’s try it. We can always revert.

Yes:

-- class methods

function EventBus:inform(message)
    if type(message) == "string" then
        message = splitStringToTable(message)
    end
    for i,msg in ipairs(message) do
        Bus:publish("addTextToCrawl", self, {text=msg})
    end
end

Now I can do this:

function Player:inform(message)
    Bus:inform(message)
end

So that’s nice.

We could enhance the InventoryItem to allow a message when used.

Let’s first see why a couple of tests are whining.

1: Create the monsters -- Monster:127: attempt to index a nil value (global 'Bus')
1: Can select monsters by level  -- Actual: 5, Expected: 7

The first is pretty clear: we don’t have an EventBus initialized inn those tests. The second is because I’ve set the Ankle Biter to be a level one monster, but have turned off a bunch of others. I’m not quite clear how we get five. Let’s see the code:

        _:test("Can select monsters by level", function()
            local t = Monster:getMonstersAtLevel(2)
            _:expect(#t).is(7) -- four #1, three #2
        end)

I’m thinking that will come back when I turn off my handy debug settings. But if I want to commit, I have to do that. OK.

Bizarrely, I get a new batch failing:

3: rollDamage  -- Actual: 2, Expected: 4
3: rollDamage  -- Actual: Spider takes 2 damage!, Expected: Spider takes 4 damage!

Oh, I think I know what may be going on. I think we have a fake RNG going in here.

        _:test("rollDamage", function()
            local result
            local i,r
            local player = FakeEntity("Princess")
            local monster = FakeEntity("Spider")
            randomNumbers = {4}
            local co = CombatRound(player, monster, fakeRandom)
            co:rollDamage()
            result = co.commandList
            i,r = next(result,i)
            _:expect(r.op).is("extern")
            _:expect(r.receiver).is(monster)
            _:expect(r.method).is("damageFrom")
            _:expect(r.arg1).is(4)
            i,r = next(result,i)
            _:expect(r.op).is("display")
            _:expect(r.text).is("Spider takes 4 damage!")
        end)

Yes. We tweaked CombatRound:rollDamage, didn’t we?

function CombatRound:rollDamage()
    if self.attacker.rollDamage then
        damage = self.attacker:rollDamage()
    else
        local damage = self.random(1,6)
    end
    self:applyDamage(damage)
end

We’re now allowing attackers to have their own rollDamage, and Monster has it. Let’s reduce the test a bit, to read out the damage as rolled rather than try to force it.

Had to fix this to use the local correctly:

function CombatRound:rollDamage()
    local damage
    if self.attacker.rollDamage then
        damage = self.attacker:rollDamage()
    else
        damage = self.random(1,6)
    end
    self:applyDamage(damage)
end

The test was actually finding a bug. And then I modify the test to accept whatever is rolled:

        _:test("rollDamage", function()
            local result
            local i,r,rr
            local player = FakeEntity("Princess")
            local monster = FakeEntity("Spider")
            --randomNumbers = {4}
            local co = CombatRound(player, monster)
            co:rollDamage()
            result = co.commandList
            i,r = next(result,i)
            _:expect(r.op).is("extern")
            _:expect(r.receiver).is(monster)
            _:expect(r.method).is("damageFrom")
            --_:expect(r.arg1).is(4)
            i,rr = next(result,i)
            _:expect(rr.op).is("display")
            _:expect(rr.text).is("Spider takes "..r.arg1.." damage!")
        end)

Test runs. I remove the fake random number function (just commented out, call me old school) and mark it as unused in CombatRound init:

function CombatRound:init(attacker,defender, rngNotUsed)
    self.attacker = attacker
    self.defender = defender
    self.random = rngNotUsed or math.random
    self.commandList = {}
end

I don’t think that will come back, but at the time we used it, I wanted to be able to know what the RNG was going to produce.

Tests green. Commit: Rock InventoryItem no longer calls through Player, instead publishes “dullSharpness” direct from Inventory. Still need use-specific message, see strong letter to follow.

Now let’s add a usedMessage element to the InventoryItem.

function InventoryItem:init(aTable)
    self.icon = aTable.icon
    self.name = aTable.name or self.icon
    self.object = aTable.object or self
    self.message = aTable.method or "print"
    self.description = aTable.description or self.name
    self.used = aTable.used or nil
    self.attribute = aTable.attribute
    self.value = aTable.value
end

I set it explicitly nil as an attempt at clarity. YMMV.

Now to use it:

        InventoryItem{ icon="rock", name="Rock", object=Bus, method="publish", attribute="dullSharpness", description="Mysterious Rock of Dullness", used="You have dulled all the sharp objects near by." },

Now to make it appear:

function InventoryItem:touched(aTouch, pos)
    if aTouch.state == ENDED and manhattan(aTouch.pos,pos) < ItemWidth//2 then
        Inventory:remove(self)
        self:informObjectRemoved()
        self:usedMessage()
        self.object[self.message](self.object, self.attribute, self.value)
    end
end

function InventoryItem:usedMessage()
    if self.used then
        Bus:inform(self.used)
    end
end

Sure enough, the message comes out:

two messages

Super. Commit: Rock now displays “You have dulled all the sharp objects near by”. All Ankle Biters presently alive will be dulled.

Let’s review and call it a morning. I’ll publish this on Sunday, to allow for Juneteenth voices to be better heard today.

Review

We haven’t done anything about our “outline” idea, but we do have a better sense of the things that need to come together. And we’ve made the overall job easier, by allowing inventory items direct access to Bus:publish, which will let them have their affect without being mediated by the Player. We’ll want to change all the Items to work that way if they can.

So we’ve learned a little and improved the code a little. Not bad for a Saturday. Now I’m gonna go see what Juneteenth has to offer in terms of ideas and learning.

See you next time!


D2.zip