Ron, why do you write this stuff? And let’s get that Pathfinder guy working better.

At the Zoom Ensemble last night, we didn’t talk much about code at all. We didn’t actually look at any. We mostly spoke about the things we all share, as what I might call “agilists”, and of course we grouched and kvetched about abuses, avoidance, and folks just not knowing.

I had just written an email to a well-known thinker in software, explaining why I was “inexplicably” not very interested in their current work. We talked about that and about how people come to an understanding of the things we all talk, write, and teach about.

With the help of GeePaw Hill, who often says things that initially make no sense to me, I’ve come to a somewhat better understanding of what I’m doing.

Wisdom begins when we learn the difference between “That makes no sense”, and “I don’t understand”.
– Mary Doria Russell

The idea that initially stymied me was GeePaw’s notion that people do not learn “this stuff” by the transmission of information. I think I finally get what he’s at.

Now clearly we do pass on something that acts like information. I might define a term and you might remember it. I might recommend a book and you might learn some terms from it. But someone’s definitions, some book’s terms, will not teach us how to do a thing.

Doing the thing will teach us how to do the thing.

And even doing it isn’t enough, though I think it is necessary. We also have to do it in the way it’s “intended” to be done, and we have to attend to the feedback we get from doing the work while doing the thing.

We have to think the right thoughts while we do the thing. We have to listen to the work as we do it. We have to feel the way the work responds to our working.

So what?

So, when I write these things, I show you what I do, I tell you as best I can what I’m thinking and feeling, and I try to let you see the results in the code … and the results in me.

I do that so that, once in a while, a reader will decide to try something that they saw here, or that something they saw here reminded them of, or something entirely new … and listen to their own senses as they do it … and thereby learn something.

I don’t even care if it’s the thing I thought I was writing about.

This work–and maybe this life–is about moving in the world, working in the world, and sensing how the world and work respond, and adjusting for best results.

What are krill, anyway?

Sometimes I say that I just try to swim where the krill are plentiful and tasty.

I’m here to show you where and how I swim, and to tell you how the krill seem today.

Let’s Munch Some Krill

Yesterday, we built our first cut at a monster who moves from wherever it is, to the WayDown. The idea will be that once in a while, one of these things appears, and you can follow it to find the next level. Or, perhaps, follow it into a trap: it could lead you anywhere.

I don’t want to give this monster massive intelligence. It knows, for every tile in the game, a tile that is one step closer to its goal, and it tries to step there. If the Tile refuses to allow the monster to enter, the monster will be stuck. We don’t want that. We want our monster to be able to enter any cell, because making him recompute a new path isn’t much fun.

I plan to make the monster looks like a puff of smoke or the like, so it will not be incredible that it can waft right over a chest or spikes or another monster. What we want to do today, is simply to allow our monster to step to its next tile without being blocked by anything that’s in there.

This ought to be easy. If it isn’t, there’s something not right. Let’s find out.

The Monster class has an array of moves that reflect the various “intentions” that a monster might have in moving, including:

function Monster:basicMaintainRangeToPlayer(dungeon)
function Monster:basicMoveAwayFromPlayer(dungeon)
function Monster:basicMoveAwayFromTile(dungeon, tile)
function Monster:basicMoveTowardTile(dungeon, tile)
function Monster:basicMoveRandomly(ignoredDungeon)
function Monster:basicMoveTowardPlayer(dungeon)
function Monster:basicMoveToTile(tile)

The last one, basicMoveToTile, is the one that our path-finding monster uses. Our mission is to change that method so that it always allows the move.

Currently, the method is:

function Monster:basicMoveToTile(tile)
    self.tile = self.tile:validateMoveTo(self,tile)
end

The validate method has the possibility of saying no. It looks like this:

function Tile:validateMoveTo(anEntity, newTile)
    if newTile:isRoom() then
        local tile = newTile:attemptedEntranceBy(anEntity, self)
        self:moveEntrant(anEntity,tile)
        return tile
    else
        return self
    end
end

The attemptedEntrance method can return either the new, proposed tile, or the tile that the entrant is currently on. We don’t want that. We want to move unconditionally. It would seem that we can just call moveEntrant.

Let’s do that.

function Monster:basicMoveToTile(tile)
    self.tile:moveEntrant(self,tile)
    self.tile = tile
end

We need to set the tile into ourselves, because moveEntrant doesn’t return the tile to which it moves. It could. Arguably it should.

This code is … let’s say “interesting”. The basic design is that an entity is on a tile and wants to go to another tile, so it calls a suitable method, typically legalMove... or validateMove..., and those methods return the tile to which the entity actually moves. The entity doesn’t know if the move “succeeded” or “failed”. It just knows where it wound up.

We’re calling a method that is at the bottom of that call chain, because we want to just move, dammit. The fact that this method isn’t following the convention of returning the tile is a bit iffy. We’ll revisit this question. But for now, I want to see it work.

Watching it work, I have good news and bad news. The good news is that the frog does march right over obstacles. When it looks like a cloud, that should look just fine. The bad news is, and it should be no surprise, when it reaches the WayDown, that’s the end of the path, and when it asks for the next tile from the map, the answer is nil. It’s the end of the list.

I knew we had to do something about this, but I didn’t reflect on what was going to happen. Before today’s change, the frog would continually try to move to the WayDown but it couldn’t: the move was refused. Now, it can.

What I would like to have happen is for the monster, be it a cloud or a frog, to step onto the WayDown, and then in its next turn, just disappear.

We don’t have monsters that just disappear. We can have them die, and if they do, they leave their husk lying about. We want this guy to be gone.

Darn. This may require some thinking. I really shouldn’t commit this code until we fix this.

When an entity is on a given tile, it is in that tile’s contents. That’s what’s used to determine if there’s a fight to be had, or other interactions. And, like all monsters, our pathfinding monster is in the Monsters collection, from which it is given chances to move and chances to draw itself.

So, we need to remove our monster from the tile, and from the Monsters collection. An entity knows the GameRunner, its own tile, and a bunch of info about itself. So we’ll have to talk to the GameRunner to do this job.

But the information about how to move is in the monster’s movement strategy, in our case here:

PathMonsterStrategy = class()

function PathMonsterStrategy:init(monster, map)
    self.monster = monster
    self.map = map
end

function PathMonsterStrategy:execute(dungeon)
    local newTile = self.map:nextAfter(self.monster:getTile())
    self.monster:basicMoveToTile(newTile)
    self.tile = self.monster:getTile()
end

We have to start here. The newTile can be nil, which is the signal that the path is complete. We could imagine some generic action to be done here, and since we have to say something to the monster, let’s tell it pathComplete.

function PathMonsterStrategy:execute(dungeon)
    local newTile = self.map:nextAfter(self.monster:getTile())
    if newTile then
        self.monster:basicMoveToTile(newTile)
        self.tile = self.monster:getTile()
    else
        self.monster:pathComplete()
    end
end

Now we need that method.

function Monster:pathComplete()
    self.runner:removeMonster(self)
end
function GameRunner:removeMonster(monster)
    monster:getTile():removeContents(monster)
    self.monsters:remove(monster)
end

And …

function Monsters:remove(monster)
    for i,m in ipairs(self.table) do
        if m == monster then
            table.remove(self.table,i)
            return
        end
    end
end

We have to search for him, as this table is an array. This should do the job.

frog goes down

That works as intended. Commit: pathfinder appears to go down the WayDown.

How Were Those Krill?

OK, was that tasty, or not? I’d have to say that it went in nicely, and without any big surprises and without changes that feel like hackery. But there are some not quite delicious tastes in here.

Correlated Changes
To remove a monster, we have to do two things. We have to remove it from the Monsters table, and we have to remove it from the contents of its tile. If we fail to do them both, bad things will happen. This is what we call “coupling”, and it’s not a good thing, though it is often a necessary thing.
Going Through GameRunner
The game is designed to be rather centralized around the GameRunner, which either knows a thing, or knows someone who knows the thing. This means that even when we’re quite near the objects that need manipulating, we usually have to ask the GameRunner to intervene. It’s like a central phone exchange. Distributed control might reduce connections.
No Tests
I have no tests for this kind of thing. My only recourse is to play the game and see what happens. Often that’s not too big a deal but sometimes I have to search the dungeon to find a treasure that I want to test, or, in this case, follow a frog for ages to find out what it’s going to do. It would be better to have tests, but I don’t know how to write them.

It might be better to say: I’ve built a system that is hard to test in some important ways. An important question is what would make the system easier to test. Better tests would give me more confidence, and would speed up my work.

Big Program
This is rather a large program, though far from the largest program ever written in Codea. But I have–let me count them–37 tabs of code. Codea displays those in one long row at the top of the screen, as shown below. This is becoming hard to manage. It takes a while to get to the tab one wants: there’s no good navigation. All things considered, Codea is after all just an iPad app.

top row

Changes Touch Multiple Objects
Today’s change touched four tabs: GameRunner, Monsters, Monster, and MonsterStrategy. It was only about 25 lines of actual changes. Given the design, it all makes sense, and the changes were small and seem righteous to me, but I’d hope for changes to fewer objects than this.

You can imagine that with a different structure, we could have changed only the strategy, to decide to remove the monster, and the monster, to remove itself. Or maybe there’d be some kind of event structure that broadcasts information to people who need to know things.

Of course, there’s always a better design to be had, and better code to be had. The question before us is always to find a balance between doing the new system capabilities that we desire, versus moving the system around to make it easier to work with, versus improving our tools.

Bottom-ish line, the only thing that really bugs me about today’s work, and much of the work like today’s work, is that I don’t have decent tests for it. Often, given what I know, I can’t even think how I would like to test a thing. Yesterday, I couldn’t even think what the text could be like.

I admit to sometimes feeling like after nearly 60 years programming, I should know exactly what test to write, exactly what design to use, but that’s not the nature of things. We change the code, we learn, rinse, repeat.

Let’s Improve Something

I don’t know, anything. How about this?

function GameRunner:removeMonster(monster)
    monster:getTile():removeContents(monster)
    self.monsters:remove(monster)
end

function Monsters:remove(monster)
    for i,m in ipairs(self.table) do
        if m == monster then
            table.remove(self.table,i)
            return
        end
    end
end

Let’s make it be that the GameRunner doesn’t need to know to remove the monster from the tile.

function GameRunner:removeMonster(monster)
    self.monsters:remove(monster)
end

Now he just forwards the message. That makes sense. Now, at this moment, we need to do this:

function Monsters:remove(monster)
    for i,m in ipairs(self.table) do
        if m == monster then
            table.remove(self.table,i)
            monster:getTile():removeContents(monster)
            return
        end
    end
end

I think that’s better, because the decision is closer to the monster. But we can get closer:

function Monsters:remove(monster)
    for i,m in ipairs(self.table) do
        if m == monster then
            table.remove(self.table,i)
            monster:remove()
            return
        end
    end
end

And then …

function Entity:remove()
    self.tile:removeContents(self)
end

That is a refactoring. Behavior should not change. I’ll check of course. And it works. What is slightly odd now is that we started here:

function Monster:pathComplete()
    self.runner:removeMonster(self)
end

We call up to GameRunner, down to Monsters (remove from table) then back down to Monster(Entity). However, all the code along the way seems to pertain to the object that does the work.

I think this is better.

Commit: improve monster removal code.

One More Thing

Let’s see about changing the look of our pathfinder to more of a cloud. Maybe we have some art like that.

A search of my files shows me a folder with “smokeparticleassets”. It has one folder called “White puff” with 25 (!) white puffs in it. That’s the good news. The not quite so good news is that they are about 130kB each. They’re each about 350 or 400 pixels square. I need them to be more like 60ish, so if I resize them I can crunch that down to 4 or 5K each.

There ought to be an easy way to do this, but I can’t find it. There’s an Image Size app, but it only works on photos, not files. I can probably do this in Procreate.

OK, not too hard. Import each to Procreate, Tools Crop & Resize, Resample, set size 64, hit Done, repeat. I did four of them, which should be enough for a test. Share to files and store in Codea’s Dropbox Assets.

Now I can move them and make a monster.

function Monster:getPathMonster(runner, map, tile)
    local mtEntry = {name="Cloud", level=99, health={20,20}, speed={20,20}, strength={20,20},
    attackVerbs={"wafts"}, dead=asset.whitePuff00,
    moving={asset.whitePuff00, asset.whitePuff01, asset.whitePuff02, asset.whitePuff03}}
    local monster = Monster(tile, runner, mtEntry)
    monster:setMovementStrategy(PathMonsterStrategy(monster,map,tile))
    return monster
end

OK, well, that wasn’t QUITE what I needed. These guys aren’t transparent. I think I messed up the export. A bit more messing around deleting and moving files and it’s lined up. But it’s not changing the pattern, because I haven’t started this monster’s animations.

I look around and modify monster init to start the timers.

The cloud works nicely:

cloud

I notice that monsters all kind of change pose in sync. Let’s randomize that, but first commit: path monster is a cloud.

Here’s our code for setting the timer:

function Monster:setAnimationTimer()
    if self.animationTimer then tween.stop(self.animationTimer) end
    self.animationTimer = self:setTimer(self.chooseAnimation, 0.5, 0.05)
end

function Monster:setTimer(action, time, deltaTime)
    if not self.runner then return end
    local t = time + math.random()*deltaTime
    return tween.delay(t, action, self)
end

It’s broken out because there can be other timers, though there are none right now, because I removed the movement timer.

The time is set to a half second plus a value between 0 and 0.05 seconds. Not much variability there. Let’s tweak:

function Monster:setAnimationTimer()
    if self.animationTimer then tween.stop(self.animationTimer) end
    self.animationTimer = self:setTimer(self.chooseAnimation, 0.25, 0.25)
end

faster

That’s better, and they’re certainly not in sync. I’d kind of like the cloud to be much faster, though. Let’s see about giving the monsters an optional animation timer.

function Monster:setAnimationTimer()
    if self.animationTimer then tween.stop(self.animationTimer) end
    local times = self.animationTimes or {0.25,0.25}
    self.animationTimer = self:setTimer(self.chooseAnimation, times[1], times[2])
end

Since there is no animationTimes member variable, this has no effect … yet.

In Monster init, we copy from the monster table entry:

    self.animationTimer = self.mtEntry.animationTimer
    self:startAllTimers()

And in our path monster creation table:

function Monster:getPathMonster(runner, map, tile)
    sprite(asset.documents.Dropbox.whitePuff00)
    local mtEntry = {name="Cloud", level=99, health={20,20}, speed={20,20}, strength={20,20},
    attackVerbs={"wafts"}, dead=asset.whitePuff00, animationTimer = {0.1,0.1},
    moving={asset.whitePuff00, asset.whitePuff01, asset.whitePuff02, asset.whitePuff03}}
    local monster = Monster(tile, runner, mtEntry)
    monster:setMovementStrategy(PathMonsterStrategy(monster,map,tile))
    return monster
end

Now the cloud should swap much more rapidly. Still not as fast as I’d like. And I convince myself that it’s not using my new values. Moved a little too quickly, I suspect. Yes. Named it Times in one place and Timer in the others. Grr.

In Init:

    self.animationTimes = self.mtEntry.animationTimes or {0.25,0.25}

And …

function Monster:getPathMonster(runner, map, tile)
    sprite(asset.documents.Dropbox.whitePuff00)
    local mtEntry = {name="Cloud", level=99, health={20,20}, speed={20,20}, strength={20,20},
    attackVerbs={"wafts"}, dead=asset.whitePuff00, animationTimes = {0.1,0.1},
    moving={asset.whitePuff00, asset.whitePuff01, asset.whitePuff02, asset.whitePuff03}}
    local monster = Monster(tile, runner, mtEntry)
    monster:setMovementStrategy(PathMonsterStrategy(monster,map,tile))
    return monster
end

And …

function Monster:setAnimationTimer()
    if self.animationTimer then tween.stop(self.animationTimer) end
    local times = self.animationTimes
    self.animationTimer = self:setTimer(self.chooseAnimation, times[1], times[2])
end

That’s pretty good:

pretty good

But I’d like it not to look so cyclic. You can see it just has four states repeating. I’ll modify the table to use a couple of them over. That’ll do for now:

six states

That’s six states, two reused. I can live with it.

Commit: cloud cycles faster.

Let’s Sum Up

Today has gone nicely, and we have a sweet new cloud that floats along and leads us to the WayDown. We’ll want to make it show up more or less randomly, or maybe it only shows up with the player is “ready”. We’ll figure out what makes for interesting game play.

And we can use this basic pattern for other creatures who can lead us to interesting places.

The code is a little more clear, and we have a very nice new capability. And the system showed itself to be pretty readily malleable with respect to today’s needs, though I am still concerned about testing these creatures.

Overall, a good day. Not bad for a Saturday morning.

See you next time!


D2.zip