Will this thing never end? More on the Mimic. User(!) concerns. Still not a game, really.

164 articles on one little program. Surely more words than two of my books. I’m not sure how many were in the second book, but it was large. Both readers enjoyed it, though.

One(!) of my users(!), Ubergoober by name, objected to the fact that the spikes do some damage even when they are down. As in fact they do. They do more damage when up than when down:

    self.damageTable = { down={lo=1,hi=1}, up={lo=4,hi=7}}

So. One point when down, four to seven when up. Dungeons are dangerous, what can I say? Ubergoober is also running on an iPhone, and that gives him trouble seeing things sometimes. I’ve never even tried that. I think of this as an iPad Pro 12.9 game. I’ll try to get psyched to run it on my iPhone and see what happens. I’m sure it will be weird and irritating.

Ubergoober suggested that it should be possible, with careful observation, to tell the difference between a Mimic and a Chest. At this point, my plan is different. I plan to make them indistinguishable, and to give the Mimic a unique treasure that you can only get if you wake it up. I’m not sure if you have to defeat it, or if it just drops the treasure when it wakes. Maybe there are two items.

So one thing here is to make the Chest and the Mimic look the same when the Mimic is hiding.

The dimensions of the “hide” spritesheet are 958x127 (are you serious??), so the individual sizes must be about 96x127. Let’s plug that in for the Chest and see how it goes.

function Chest:draw(tiny, center)
    if tiny then return end
    pushMatrix()
    pushStyle()
    spriteMode(CENTER)
    sprite(Sprites:sprite(self.pic),center.x,center.y-5, 55,75)
    popStyle()
    popMatrix()
end

We’ll try this:

function Chest:draw(tiny, center)
    if tiny then return end
    pushMatrix()
    pushStyle()
    spriteMode(CENTER)
    sprite(Sprites:sprite(self.pic),center.x,center.y-5, 96,127)
    popStyle()
    popMatrix()
end

While trying to find a Chest–there’s never treasure when you need it–I notice two bad things. First, the monster music plays and the attribute sheet comes up when you are near a mimic. Second, the mimic turns to face the player, even when it’s hiding. That’s a bit of a tell. But maybe we should leave that feature in …

chest

mimic

Pretty good, but the chest and mimic aren’t aligned quite the same. The chest used to use a different graphic and its y was adjusted. Remove that and they’re perfect:

both chest and mimic

Which is the Mimic and which the Chest? Moohahaha try and find out.

Actually it’s pretty easy to find out. If you hit ?? the one says “I am probably a mysterious chest.” and the other says “I am a mimic”. Let’s do something about that, and let’s try for something better than if statements.

In Monster:

function Monster:query()
    return "I'm a "..self:name()
end

Let’s give these guys some pluggable behavior. We’ll call it self.behavior for now, and there’s just one kind, called NormalMonsterBehavior.

I’m not going to start this with TDD. I’m not quite sure what I want, so I’m going to sketch in the query and then …

Wait. I was thinking that I’d create a couple of new classes, NormalMonsterBehavior and MimicMonsterBehavior, and populate them with methods that do the different things we want.

Let’s try something different. Let’s make a table of behaviors. It’ll be keyed by the desired behavior, e.g. “query”, and the values will be the method to call.

So …

function Monster:initAttributes()
    local health = CharacterAttribute(self:roll(self.mtEntry.health))
    local strength = CharacterAttribute(self:roll(self.mtEntry.strength))
    local speed = CharacterAttribute(self:roll(self.mtEntry.speed))
    local attrs = { _health=health, _strength=strength, _speed=speed }
    self.characterSheet = CharacterSheet(attrs)
    self._movementStrategy = self:selectMovementStrategy(self.mtEntry.strategy)
    self.behavior = Monster.normalBehavior
end

Monster.normalBehavior = {
    query=Monster.queryName,
}

And …

function Monster:query()
    return self.behavior.query(self)
end

function Monster:queryName()
    return "I'm a "..self:name()
end

I think this ought to work. Doesn’t. I think we can’t define the table that way, we’ll need to define it inside a method. If this works, I’ll explain my “thinking”.

OK, this works as anticipated:

function Monster:initAttributes()
    local normalBehavior = {
        query=Monster.queryName,
    }
    local health = CharacterAttribute(self:roll(self.mtEntry.health))
    local strength = CharacterAttribute(self:roll(self.mtEntry.strength))
    local speed = CharacterAttribute(self:roll(self.mtEntry.speed))
    local attrs = { _health=health, _strength=strength, _speed=speed }
    self.characterSheet = CharacterSheet(attrs)
    self._movementStrategy = self:selectMovementStrategy(self.mtEntry.strategy)
    self.behavior = normalBehavior
end

I “just” defined the table inside the initAttributes function. I could have defined it anywhere that is sure to get called. The issue, I think, is that when it was set outside a method, the table definition gets run at compile time, and at that point, the queryName function is not defined, so the table entry is nil, and therefore not there at all.

Just for fun, I’m going to try something else:

Monster.normalBehavior = {
    query=function(monster) return monster:queryName() end
}

function Monster:initAttributes()
    local health = CharacterAttribute(self:roll(self.mtEntry.health))
    local strength = CharacterAttribute(self:roll(self.mtEntry.strength))
    local speed = CharacterAttribute(self:roll(self.mtEntry.speed))
    local attrs = { _health=health, _strength=strength, _speed=speed }
    self.characterSheet = CharacterSheet(attrs)
    self._movementStrategy = self:selectMovementStrategy(self.mtEntry.strategy)
    self.behavior = Monster.normalBehavior
end

That works as advertised. Let’s go with this form and see if we wind up liking it.

Now we want this behavior to be pluggable, so we want a new table for the Mimic:

Monster.normalBehavior = {
    query=function(monster) return monster:queryName() end
}

Monster.mimicBehavior = {
    query=function(mimic) return "I am probably a mysterious chest." end
}

And in the init …

function Monster:initAttributes()
    local health = CharacterAttribute(self:roll(self.mtEntry.health))
    local strength = CharacterAttribute(self:roll(self.mtEntry.strength))
    local speed = CharacterAttribute(self:roll(self.mtEntry.speed))
    local attrs = { _health=health, _strength=strength, _speed=speed }
    self.characterSheet = CharacterSheet(attrs)
    self._movementStrategy = self:selectMovementStrategy(self.mtEntry.strategy)
    local behavior = self.mtEntry.behaviorName or "normalBehavior"
    self.behavior = Monster[behavior]
end

And, finally, in the monster definitions:

    m = {name="Mimic", level=1, health={10,10}, speed={10,10}, strength=10, facing=-1, strategy=MimicMonsterStrategy, behaviorName="mimicBehavior",
        attackVerbs={"bites", "chomps", "gnaws"},
        dead={"mdead01","mdead02","mdead03","mdead04","mdead05","mdead06","mdead07","mdead08","mdead09","mdead10"},
...

This one has to be a string to make this work. A bit tricky, perhaps? Let’s make it work tho. And I think it should.

mimic works

So that’s as intended. I could imagine making him reply with “HAHAHA I’m a Mimic!!!” If he’s awake. Should we?

Yes, let’s.

Monster.mimicBehavior = {
    query=function(mimic) 
        if mimic.awake then
            return "HAHAHA!!! I'm a MIMIC!!!"
        else
            return"I am probably a mysterious chest."
        end
    end,
}

That does the trick:

haha

OK, now we want to stop the flipping, which is done here:

function Monster:drawMonster()
    if not self.tile.currentlyVisible then return end
    pushMatrix()
    pushStyle()
    spriteMode(CENTER)
    noTint()
    local center = self.tile:graphicCenter()
    translate(center.x,center.y)
    self:flipTowardPlayer()
    self.animator:draw()
    popStyle()
    popMatrix()
end

Here again we want a call to behavior:

function Monster:drawMonster()
    if not self.tile.currentlyVisible then return end
    pushMatrix()
    pushStyle()
    spriteMode(CENTER)
    noTint()
    local center = self.tile:graphicCenter()
    translate(center.x,center.y)
    self.behavior.flip(self)
    self.animator:draw()
    popStyle()
    popMatrix()
end

And in our tables:

Monster.normalBehavior = {
    query=function(monster) return monster:queryName() end,
    flip=function(monster) monster:flipTowardPlayer() end,
}

Monster.mimicBehavior = {
    query=function(mimic) 
        if mimic.awake then
            return "HAHAHA!!! I'm a MIMIC!!!"
        else
            return"I am probably a mysterious chest."
        end
    end,
    flip = function(mimic) if mimic.awake then mimic:flipTowardPlayer() end end
}

That works as planned, for both Mimics and regular monsters:

mimic no flip

slime facing right

slime facing left

OK, now for not displaying the attribute sheet. That’s done here:

function Monster:drawExplicit()
    local r,g,b,a = tint()
    if r==0 and g==0 and b==0 then return end
    self:drawMonster()
    self:drawSheet()
end

As before:

function Monster:drawExplicit()
    local r,g,b,a = tint()
    if r==0 and g==0 and b==0 then return end
    self:drawMonster()
    self.behavior.drawSheet(self)
end

Monster.normalBehavior = {
    query=function(monster) return monster:queryName() end,
    flip=function(monster) monster:flipTowardPlayer() end,
    drawSheet=function(monster) monster:drawSheet() end,
}

Monster.mimicBehavior = {
    query=function(mimic) 
        if mimic.awake then
            return "HAHAHA!!! I'm a MIMIC!!!"
        else
            return"I am probably a mysterious chest."
        end
    end,
    flip = function(mimic) if mimic.awake then mimic:flipTowardPlayer() end end,
    drawSheet = function(mimic) if mimic.awake then mimic:drawSheet() end end,
}

mimic does not draw sheet

As we see in the movie above, the mimic doesn’t draw its attribute sheet until it wakes up.

This is good. I think we’re due a commit here, and then some reflection. Commit: mimic pretends to be a chest, doesn’t flip when hiding, doesn’t display attributes when hiding.

Reflection

This pluggable behavior is working nicely. A special table can be selected for any monster using the “behaviorName” entry in its definition table. The tables are keyed collections of functions:

Monster.normalBehavior = {
    query=function(monster) return monster:queryName() end,
    flip=function(monster) monster:flipTowardPlayer() end,
    drawSheet=function(monster) monster:drawSheet() end,
}

Monster.mimicBehavior = {
    query=function(mimic) 
        if mimic.awake then
            return "HAHAHA!!! I'm a MIMIC!!!"
        else
            return"I am probably a mysterious chest."
        end
    end,
    flip = function(mimic) if mimic.awake then mimic:flipTowardPlayer() end end,
    drawSheet = function(mimic) if mimic.awake then mimic:drawSheet() end end,
}

The normal table just calls the method that was originally used prior to pluggable behavior. The behaviors all expect to be passed the monster as their parameter, so they can call methods on the monster in question.

We use the behavior like this:

function Monster:query()
    return self.behavior.query(self)
end

For some reason, we can’t say this instead:

function Monster:query()
    return self.behavior:query()
end

I would have said that the latter was just syntactic sugar for the former, but anyway it doesn’t work. I’ll have to think about why. Be that as it may, the former form works for us.

Oh, I understand why. foo:query() is equivalent to query(foo), so the function gets the behavior table as self. So that can’t work, but the way it does work is consistent.

Is this too obscure?

One might argue that this is a bit too deep in the bag of tricks, but since functions are first-class objects in Lua, I think our Lua-oriented team would be quite ready to use tables of functions in this way.

In fact, an object in Lua is little more than a table of functions.

So I’m arguing that this is a rather elegant approach to specialized behavior in the Monster, and that we might want to consider using it in other places where we want variable behavior in the system.

We have one more issue, which is that the monster music plays if you are near a Mimic. We could leave that, as a clue, or we could turn it off. Let’s turn it off, because that may be a bit tricky, and then we can readily turn it back on if we wish.

The music is controlled here:

MonsterPlayer = class()

function MonsterPlayer:init(runner)
    self.runner = runner
    local playlist = {battle=asset.downloaded.A_Hero_s_Quest.Battle, dungeon=asset.downloaded.A_Hero_s_Quest.Dungeon}
    self.sp = SmoothPlayer(3, playlist)
    tween.delay(1, self.everySoOften, self)
end

function MonsterPlayer:checkForDanger()
    local close = self.runner:hasMonsterNearPlayer(5)
    if close then self.sp:play("battle") else self.sp:play("dungeon") end
end

function MonsterPlayer:everySoOften()
    self:checkForDanger()
    tween.delay(1, self.everySoOften, self)
end

If we were to do something here, we’d need to get ahold of all the monsters found by hasMonsterNearPlayer. Let’s look in there:

function GameRunner:hasMonsterNearPlayer(range)
    return self.monsters:hasMonsterNearPlayer(range)
end

function Monsters:hasMonsterNearPlayer(range)
    for i,m in ipairs(self.table) do
        if m:isAlive() and m:manhattanDistanceFromPlayer() <= range then return true end
    end
    return false
end

Let’s ask a different question:

function Monsters:hasMonsterNearPlayer(range)
    for i,m in ipairs(self.table) do
        if m:isActive() and m:manhattanDistanceFromPlayer() <= range then return true end
    end
    return false
end

Now we can do isActive in Monster.

function Monster:isActive()
    return self.behavior.isActive(self)
end

And in the tables:

Monster.normalBehavior = {
    query=function(monster) return monster:queryName() end,
    flip=function(monster) monster:flipTowardPlayer() end,
    drawSheet=function(monster) monster:drawSheet() end,
    isAlive=function(monster) return monster:isAlive() end,
}

Monster.mimicBehavior = {
    query=function(mimic) 
        if mimic.awake then
            return "HAHAHA!!! I'm a MIMIC!!!"
        else
            return"I am probably a mysterious chest."
        end
    end,
    flip = function(mimic) if mimic.awake then mimic:flipTowardPlayer() end end,
    drawSheet = function(mimic) if mimic.awake then mimic:drawSheet() end end,
    isAlive = function(mimic) return mimic.awake and mimic:isAlive() end,
}

I think this probably works. (Probably?? What is this “probably”???)

Also, it doesn’t because I said isAlive and should have said isActive:

Monster.normalBehavior = {
    query=function(monster) return monster:queryName() end,
    flip=function(monster) monster:flipTowardPlayer() end,
    drawSheet=function(monster) monster:drawSheet() end,
    isActive=function(monster) return monster:isAlive() end,
}

Monster.mimicBehavior = {
    query=function(mimic) 
        if mimic.awake then
            return "HAHAHA!!! I'm a MIMIC!!!"
        else
            return"I am probably a mysterious chest."
        end
    end,
    flip = function(mimic) if mimic.awake then mimic:flipTowardPlayer() end end,
    drawSheet = function(mimic) if mimic.awake then mimic:drawSheet() end end,
    isActive = function(mimic) return mimic.awake and mimic:isAlive() end,
}

That works. Music plays on regular monsters, doesn’t play for closed Mimics, does play when the Mimic opens.

I notice another issue. When Chests open, we can’t see the top part, which I personally drew at great personal expense. This is because large objects need to be drawn after all the tiles, not just the tile they are on. I think for now, I’ll let that go. I’ll make a yellow bug report for it: can’t see top of open chest.

Commit: Mimics don’t evoke danger music when hiding.

Now we do have an issue that showed up in this most recent change: if we put a typo or other mistake into one or both of the behavior tables, it won’t be detected until the feature is executed. That could mean that a defect could slip in … since I had my “typo” in both tables, it was found as soon as I came upon a monster. But suppose it had only been in the Mimic table. If I tested the game but never encountered a Mimic, the bug could have slipped into the release.

In the case in hand, my method that used the table called one function name and the table had the function stored under another name. There’s really no good way to check that mechanically.

We could, however, at least ensure that all the same names occur in both tables. I’ll make a note of that, but I think we’ll wrap this up for today.

Quick Summary

We’ve come up with a nice form of pluggable behavior that allows us to customize our Mimic monster without subclassing and without a bunch of if statements. (We do still check him for awake, but we aren’t checking the monster type as we would otherwise have to do. That reminds me, was there a check for that somewhere?

function Player:startActionWithMonster(aMonster)
    local name = aMonster:name()
    if name ~= "Mimic" or (name == "Mimic" and  aMonster.awake) then
        self.runner:initiateCombatBetween(self,aMonster)
    else
        aMonster.awake = true
    end
end

Indeed there is. This is a tricky one. I’ve made yet another note. I’m sure we can clean that one up, but not today. Today I gotta get outa here.

Anyway, I like this. If you have a view, please let me know.

See you next time!


D2.zip