I ‘created’ new art for an open chest. Then let’s see if we can make the Mimic behave differently depending on its mood.

On my living room iPad, in Procreate–a great program, by the way–I imported one of the Mimic’s images that looks like a chest, and then drew on top of it, until I got something that looks like an open chest, at least down at the scale of the game.

open

I saved it, and shared it as PNG, back into my downloads folder, which makes it appear on this iPad. I copied it to Codea’s Dropbox.assets folder and thence into D2.

function Chest:init(tile, runner)
    tile:moveObject(self)
    self.runner = runner
    self.closedPic = "mhide02"
    self.openPic = asset.open_chest
    self.pic = self.closedPic
    self.opened = false
end

Now it will show up as the picture when the Chest is open. Note the two different forms for defining a pic to be used in the Codea sprite function. The latter is the usual form, referring to a stored “asset”, a PNG file in this case. The former, identified because it is a string, refers to a slice of a sprite sheet, which has been sliced and diced by my Sprites and SpriteSheet objects.

When I go to display a sprite, it goes like this:

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

Looking at that, I remember that I preferred a different y alignment for the Chest:

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

There seem always to be these little tweaks to get my images to look good. If this were a real game, they’d all be standardized with common size and centers, but since I’ve been grabbing them from all over, they need adjusting.

Anyway, the code Sprites:sprite(self.pic) checks to see if its parameter is a string or not. If not, it’s an asset, and it gets returned. If it is a string, the string is looked up in the table of sprites and returned:

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

So now, if my luck is in, the chests should open.

chest opens

So that’s good. I pressed the ?? button to get the chest’s message. The irony1 is that I expect that mimics will have the same message, if we have them laying around in chest form. That is yet to be decided.

In the video above you’ll have noticed that the prize, a health power up, appears on top of the chest. I anticipate changing that shortly.

It occurs to me that the Chest could probably be a Decor just as well as a separate object. This might be the time to do that. First we’ll commit what we have: Chest uses Mimic icon and open chest icon.

Let’s look at Decor. First, a tiny musing:

Plans

Every day I start out with a plan. And every day, I do something other than the plan. Sometimes I don’t even start on much of the plan. Sometimes I don’t start on it at all, because a broken test reminds me, or something catches my eye2.

I think that is OK. As we work on whatever we planned to work on, we discover that the code has information for us that we didn’t have at the surface of our mind. We do well to heed that information, within reason.

By “within reason”, I mostly mean that we need to keep our eye on what is really good for the product. We could go down some refactoring rat hole, improving mass quantities of code, with no real benefit for the user or the company we work for. That would not be good. But we should expect that our plans will change as we discover, or rediscover, what the code actually wants.

Decor

So could we use Decor to serve as the chest? There are issues. Decor does not change its look (though it might be interesting to jumble up the bones after you go pawing through them). Decor does not ever spawn a monster (though Chest doesn’t do that either).

Still they have a lot in common.

But I think this is not the time. Presently, we have a fairly simple situation: the Chest looks like a mimic, and we either want some chests to spawn mimics, or we want mimics to sit still and look like Chests.

But let’s do refresh ourselves on Decor. Here are some salient bits:

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

function Decor:draw(tiny, center)
    if tiny then return end
    pushMatrix()
    pushStyle()
    spriteMode(CENTER)
    translate(center.x, center.y)
    scale(self.scaleX, 1)
    sprite(self.sprite,0,0, 50,50)
    popStyle()
    popMatrix()
end

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

They give items. That’s something we want to have happen: We are supposed to change from instant power up treasures to treasures that go into inventory to be used when you need them.

Looking at it in the clear light of day, I think we’ll continue to work the chest-mimic problem in Chest and Monster. When that’s stable, we can look again at merging the two classes, Chest and Decor. For now, I’d like to have things separated out, which is generally a good thing.

I’m glad we had this little chat.

When is a Chest not a Chest?

I see two main ways we could go. One is that some Chests essentially create a Mimic when they are opened. Then the Chest would remove itself, leaving the Mimic in place.

The other way would be to have Mimics lying about, in a quiescent state, looking like a chest. When aroused, they’d wake up and start moving. There’s a nice possibility for some fun sequencing with all the images in the Mimic sprite sheets I bought. It might go like this:

  1. The Mimic is static, with the same image as a closed Chest.
  2. If you try to move into the Mimic, it wakes up. It has a sequence of animations in its “hide” mode, where it kind of unwraps from the chest form.
  3. It’s awake. It has a few modes of operation: a. Player far enough away: walk randomly b. Player near: walk toward player c. Player adjacent: attack player d. Defeated: run death animation

Here’s the waking up sequence:

hide

And here’s death:

death

We can certainly make the behavior. I think we actually have Monster Strategy methods that can do most of it. We may want some new monster attributes controlling the distance checking. Just now, monsters don’t have different animation sequences that depend on their mood. We’ll need that for the mimic.

Let’s review Monster and the strategy.

Each monster strategy is a separate class. For example:

CalmMonsterStrategy = class()

function CalmMonsterStrategy:init(monster)
    self.monster = monster
end

function CalmMonsterStrategy:execute(dungeon)
    local method = self:selectMove(self.monster:manhattanDistanceFromPlayer())
    self.monster[method](self.monster, dungeon)
end

function CalmMonsterStrategy:selectMove(range)
    if range > 10 then
        return "basicMoveRandomly"
    elseif range >= 4 then
        return "basicMoveTowardPlayer"
    elseif range == 3 then
        return "basicMaintainRangeToPlayer"
    elseif range == 2 then
        return "basicMoveAwayFromPlayer"
    elseif range == 1 then
        return "basicMoveTowardPlayer"
    else
        return "basicMoveAwayFromPlayer"
    end
end

We could readily change the range parameters, providing them, optionally, in the monster entry table, and passing them to the strategy. Let’s look at some more strategies:

NastyMonsterStrategy = class()

function NastyMonsterStrategy:init(monster)
    self.monster = monster
end

function NastyMonsterStrategy:execute(dungeon)
    local method = self:selectMove(self.monster:manhattanDistanceFromPlayer())
    self.monster[method](self.monster, dungeon)
end

function NastyMonsterStrategy:selectMove(range)
    if range > 10 then
        return "basicMoveRandomly"
    else
        return "basicMoveTowardPlayer"
    end
end

This strategy moves randomly unless the player is near by, in which case it attacks. (basicMoveTowardPlayer will attack if you are adjacent.)

HangoutMonsterStrategy = class()

function HangoutMonsterStrategy:init(monster, targetTile)
    self.monster = monster
    self.targetTile = targetTile
    self.min = 3
    self.max = 6
end

function HangoutMonsterStrategy:execute(dungeon)
    local method = self:selectMove(self.monster:manhattanDistanceFromTile(self.targetTile))
    self.monster[method](self.monster, dungeon, self.targetTile)
end

function HangoutMonsterStrategy:selectMove(range)
    if range < self.min  then
        return "basicMoveAwayFromTile"
    elseif self.min <= range  and range <= self.max then
        return "basicMoveRandomly"
    else
        return "basicMoveTowardTile"
    end
end

This strategy makes the monster stay near a given tile. It’s used for the Poison Frogs that guard the WayDown. If you’re too close to your tile you move away, if you’re just right you move randomly, and otherwise (too far away) you move toward your tile.

I suspect we’d do well to create a single MimicStrategy that causes mimics to dance as we wish.

Let’s do that.

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()
end

function Monster:selectMovementStrategy()
    if math.random(1,5) == 5 then
        return NastyMonsterStrategy(self)
    else
        return CalmMonsterStrategy(self)
    end
end

Let’s add a strategy tag to monsters, optionally and usually nil, and give the Mimic a MimicStrategy (as yet undefined).

    m = {name="Mimic", level=1, health={10,10}, speed={10,10}, strength=10, facing=-1, strategy=MimicMonsterStrategy,
        attackVerbs={"bites", "chomps", "gnaws"},
        dead="mwalk10", hit="mwalk02",moving={"mattack01", "mattack01", "mattack03", "mattack04", "mattack05", "mattack06", "mattack07", "mattack08", "mattack09", "mattack10"}
    }
    table.insert(MT,m)

Now we’ll use that if we have it:

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

We pass the strategy, possibly nil, on down:

function Monster:selectMovementStrategy(strategy)
    if strategy then return strategy(self) end
    if math.random(1,5) == 5 then
        return NastyMonsterStrategy(self)
    else
        return CalmMonsterStrategy(self)
    end
end

strategy will be a class, so we can call it and get the instance we want. This should fail looking for MimicMonsterStrategy. No, I’m wrong. There is none, it’s nil, we get the random one. Nice.

I’ll start him this way, should be easy to test:

-- MimicMonsterStrategy

MimicMonsterStrategy = class()
function MimicMonsterStrategy:init(monster)
    self.monster = monster
end

function MimicMonsterStrategy:execute(dungeon)
    local method = self:selectMove()
    self.monster[method](self.monster, dungeon)
end

function MimicMonsterStrategy:selectMove()
    return "basicMoveTowardPlayer"
end

That gives me the effect I’m looking for so far: the Mimic attacks from any range. Now we’ll want to condition its behavior a bit.

It’ll need distance from player, which we use elsewhere, so we can copy it:

function NastyMonsterStrategy:execute(dungeon)
    local method = self:selectMove(self.monster:manhattanDistanceFromPlayer())
    self.monster[method](self.monster, dungeon)
end

So …

function MimicMonsterStrategy:execute(dungeon)
    local method = self:selectMove(self.monster:manhattanDistanceFromPlayer())
    self.monster[method](self.monster, dungeon)
end

function MimicMonsterStrategy:selectMove(range)
    return "basicMoveTowardPlayer"
end

Now what I have in mind is that the Mimic won’t attack unless it is within two tiles of you. That will give the player a couple of moves to get out of the way. So let’s try this:

function MimicMonsterStrategy:selectMove(range)
    if range > 2 then
        return "basicMoveRandomly"
    else
        return "basicMoveTowardPlayer"
    end
end

I suspect this isn’t sophisticated enough but it should give us a better sense of what we want.

moving

This seems to work as advertised. The Mimics move randomly unless I get too close, and then they attack. A good first start, let’s commit: Mimics have their own strategy, attack if 2 tiles or less away.

I have a surprise appointment this morning, so let’s sum up.

Summary

We gave the Chest a charming new image to display when open. We considered whether Chests should be Decor and left the question open. It is possible that we’ll want Chests to spawn Mimics, but my present thinking is that we’ll just sprinkle them around like other monsters, and give them a behavior that causes them to look like chests when idle.

We started that process by creating a new monster strategy, MimicMonsterStrategy, and ensuring that Mimics use that strategy. The strategy itself is simplistic but with enough mechanism to remind us tomorrow what to do.

Here, again, we’re working in areas of the code that are rather clean and nicely factored. The result is that changes are pretty easy, even changes like moving from a purely random choice of monster strategy to one that is sometimes directed.

Clean code is like that.

We didn’t do any micro tests, and the code is so simple that I don’t feel the lack. We’ll see whether that comes back to bite me.

For now, another short morning but some new capability in the game. See you next time!


D2.zip

  1. I’m never sure just what “irony” actually is.

  2. SQUIRREL!!!