A new day dawns. New chai. Consolation from the Zoom Ensemble. Technical debt. And another run at combat.

It’s Saturday, it’s snowing, and it’s 0800. I have my first test glass of iced chai–sans ice–from my new Harney Chai Concentrate, made right here by my least favorite barista, me. It tastes pretty good. But the Zoom Ensemble tells me that I have to go over to the Starbucks daily and give them the dollar tip, even if I don’t buy a chai, and while I’m there, why not buy a chai anyway.

I’m not sure I can trust these guys’ advice.

I described yesterday’s debacle to Chet and Bill and Hill and Bryan and they all nodded wisely. Chet suggested that this article, number 93, should just be:

Well, I guess I can’t write a dungeon game, project cancelled, you’re all laid off, go home.

Fortunately for us all, I’m not going to take that suggestion. Well, I say “fortunately”. Let’s just say that I’m not taking the suggestion, you can assess the value of that decision on your own.

Technical Debt

I was thinking about the combat function in Dung as an example of technical debt.

Technical debt, as Ward Cunningham defined it, is emphatically not bad code. Bad code is bad code and we should avoid it and fix it when we can. But it’s not technical debt.

Technical debt is the difference between the design in the code, and the better design we now realize we’d like to have. It’s the difference in the code base between what it would be if we had known what we do now, back when we started.

Technical debt is inevitable, unless we’re not learning. We put our very best idea about XYZ into the system, we work with XYZ for a while, and we learn what a better XYZ could be.

Then, if things go as Ward suggested, we put the new learning into the code, with the usual result that the code comes together in a new, better, design form. Often it gets smaller, as the better design picks up slack and makes formerly difficult things easier.

Now, I’m not sure whether it’s fair to consider the two, three, or however many cuts we’ve made at combat to represent “technical debt”. Some probably have been technical debt: the original notion was unable to support the new requirement of the Crawl. It was perfectly adequate for what we knew when we wrote it. It just didn’t cut it for an unforeseen requirement, and we knew we needed a better design.

One more thought: in any real system, there will surely be a mix of technical debt: old designs we could do better now, and bad code. No one I know is perfect at keeping bad code out of the system.

This means that debate over whether a thing is, or is not, technical debt is perhaps interesting, but fruitless. We should notice those times when we begin to see a better design. They’re important. Classifying them carefully, not so important.

So let’s look at where we’ve been, and where we are now.

Combat and the Crawl

I keep calling that floating batch of lines that scrolls up the screen the Crawl. There are a couple of methods in the system that use the word crawl, but no objects. That is a gap between the design in my mind and the design in the code. One of the rules of simple design can be phrased as:

The code expresses all the programmers’ ideas about the code.

That’s not the case here, and we might want to remove or at least lessen that gap. I don’t think it’s holding me back. I know that the graphical part of the crawl is in Floater and the data part in Provider. If I have to come back to this program a year from now, it’s unlikely that I’ll remember that, but I’ll probably still remember Crawl. It seems to me like a term of art, even if it isn’t.

Naming aside, we have a design decision that events in the game that require a textual description will be displayed in a scrolling bit of text that floats above the action. Our Product Owner (me) has decided that it’s a hallmark of our program and that it has to work. Our programmers (me), dutiful as always, work to make it so.

The system also makes sounds and visual effects that are synchronized with what happens in the game. When she’s hit, the Princess makes an “eek” sound, and when a monster is hit, it makes a grunt. Either of them also flashes a color, yellow if they still have a lot of hit points left, red if they just have a few. And their Health declines.

When we consider these effects and the Crawl together, it becomes clear that the effects should occur at the same time that the relevant crawl line appears. All at once we should see:

“Princess hits Ghost!”
“Ghost takes 4 damage!”
grunt
Ghost health drops by 4

Now, I’ve been working on this almost non-stop since around article number 55, although not all the work between then and now was about combat. We also changed our graphics around substantially, redid the walls and halls, implemented Loot, used a new DeferredTable object to manage updating while looping, and so on. Be all that as it may, Combat has been in a floating unresolved kind of state since the first of the year, and it’s nearly Valentine’s day.

And despite yesterday’s crash-and-burn, I think we’re in good shape and nearly there. Let’s review the requirements and solution once again.

Combat / Crawl Requirements

We want important game action, such as combat, to be described in the crawl. When there are graphical or other effects associated with that action, we want those effects synchronized with the crawl.

This means that there are some effects, like “flash damage” that need to be initiated when the relevant related line comes up in the crawl, such as “Spider takes 4 damage!”.

I think we have a settled design decision on this, which is that the Crawl’s input deque is a queue of “commands”, not just text. One command is “display this”, and that puts the associated text into the crawl. But others perform system operations, like applying damage to an entity.

This means that in order to report on a combat cycle, we “just” have to put the right commands into the Crawl, in the right order. As they unwind, narration comes out and action appears on the screen. But there are some details that matter.

Notably, decisions need to be made based on what will have happened to the entities in the future. To take an example we do not yet have, imagine that there is a three-headed Cerberus monster. Imagine that it has three attacks. Now we could handle it as three monsters, but let’s imagine for now that we just want the Cerberus to make three attacks.

However, if the princess is dead, we don’t want the Cerberus to bite her dead and decaying corpus. That would be Hannibal Lecter stuff. Not suitable, very tacky. So imagine this situation for Cerb’s attacking:

  1. Head 1 bites and rolls damage on Princess. She will survive the blow.
  2. Head 2 bites and rolls damage on Princess. She will not survive the blow.
  3. What does head 3 do?

We want head 3 to growl or slaver, but not bite. How does it know? We don’t know if she’ll survive until she receives the damage, and she won’t actually receive the damage until later, when the Crawl catches up.

A wild idea has appeared!

Now we could manage all of this with some kind of buffer that tells Combat what will happen. We could record defender’s health locally, decrement it as we roll damage, and condition our behavior on the results. And, now that I’ve said it, maybe we should.

I’ve been resisting that idea because it duplicates functionality and because it can go wrong. Once we commit to such a path, we have to ensure that our future predicting code says in sync with the code that actually does apply damage and so on. But at this point, all I see is the need to track “future health”, and that’s not such a big deal. So maybe this is a simplifying idea.

What our present scheme does is different from what we might be able to do with the future-predicting approach. We don’t predict, we wait.

So when any CombatOp step runs, it may or may not emit actions into the crawl for displaying, making sounds, damaging the defender. It could use up resources, make an explosion, or anything. The next step might depend on the results of the prior one, and so what we do is emit all our environment-affecting operations and then a call back to CombatOp to do the next step. Here’s an example:

function CombatOperation:attack()
    if self.attacker:isDead() then return {} end
    local result = {}
    local msg = string.format("%s attacks %s!", self.attacker:name(), self.defender:name())
    table.insert(result, self:display(msg) )
    local cmd = { op="op", receiver=self, method="attemptHit" }
    table.insert(result,cmd)
    return result
end

What happens here in attack is that we issue two commands, one to display “Serpent attacks Princess!”, and one to call back with attemptHit. When we initiate an attack, that’s all that happens, for now … and the crawl begins to crawl. After long-time, crawl pulls the attemptHit and we trigger CombatOp to run again, doing whatever it then does, which will enqueue more displays and such, and end with another call back to CombatOp.

This repeats until a final CombatOp doesn’t enqueue a callback.

Now it turns out that inside a single CombatOp sequence there’s absolutely nothing that requires us to know the future. The first command, attack checks the attacker, and should probably check the defender. (I think we even did that at one point and reverted it out.) But after that, there is nothing whatsoever that prevents us from writing out the whole scenario.

Note, however, that if we do that, the impact on the defender will still not be applied until it gets pulled by the crawl, and that’s exactly what we want.

So What’s All the Trouble About?

But I’ve been struggling with real issues. What are they, and why am I struggling. It all seems very straightforward, so straightforward that the current code seems like too much.

The issue that triggered yesterday’s problems was that I tried to install the “counter attack” capability then, or a day or two before, and it turned out to have an unexpected side effect, namely that sometimes a dead entity could attack.

The crawl gave an important clue that I missed. It would say something like:

Spider takes 4 damage!
Spider attacks!
Spider is dead!

What that should tell us is that the spider’s counter-attack was issued too soon after the hit on the spider. I spent yesterday’s tedious session trying to find ways to be sure that the counter-attack was deferred long enough.

But the bug, I now think, was in issuing a counter-attack at all. We should leave it up to the monster, in its own turn, to attack if it wants to. I was seduced by the D&D notion that combat goes in an order and is kind of a separate session from the rest of adventuring. I took that to mean that I should implement combat as a whole.

I think it’ll work better if I implement it as one side attacking the other, and ensure elsewhere that everyone gets a chance to attack. That, done correctly, will even deal with the multiple monster situation. If everyone takes a turn, each monster can attack if it wants. We’ll deal with one attack, then any other ones.

I believe that this will require a more robust turn-based approach up in GameRunner, but we’ll get there when we get there.

What Are You Going to DO?

Good question. I think what I’m going to do is refactor the CombatOperation to do a single round of one attacker doing one attack on a defender. I think that can all be dumped into the provider as a unit.

After that works, I think we’ll find that we need to put something in place to ensure that only one entity goes at a time.

In addition, I want to make at least some of the changes from yesterday that made sense.

Let’s get started, finally.

We noticed duplication in Player and Monster:

function Player:startActionWithMonster(aMonster)
    if aMonster:isDead() then return end
    local co = CombatOperation(self,aMonster)
    self.runner:addToCrawl(co:attack())
end

function Monster:startActionWithPlayer(aPlayer)
    if aPlayer:isDead() then return end
    local co = CombatOperation(self,aPlayer)
    self.runner:addToCrawl(co:attack())
end

Those can just forward to GameRunner to remove most of the duplication.

function Player:startActionWithMonster(aMonster)
    self.runner:initiateCombatBetween(self,aMonster)
end

function Monster:startActionWithPlayer(aPlayer)
    self.runner:initiateCombatBetween(self,aPlayer)
end

function GameRunner:initiateCombatBetween(attacker, defender)
    if defender:isDead() then return end
    local co = CombatOperation(attacker,defender)
    self:addToCrawl(co:attack())
end

I think this will work as before, and it does, including the undesirable effects of attacks from both sides interleaving. Let’s commit this simple refactoring. I feel the need to have lots of save points today. Commit: refactor combat initiation over to GameRunner.

Now let’s remove the whole counterattack notion from CombatOperation.

function CombatOperation:rollDamage()
    local result = {}
    local damage = self.random(1,6)
    local op = { op="extern", receiver=self.defender, method="damageFrom", arg1=nil, arg2=damage }
    table.insert(result,op)
    table.insert(result, self:display(self.defender:name().." takes "..damage.." damage!"))
    self:counterAttack(result)
    return result
end

Remove that line before the return, and the corresponding method. Remove the repeats variable and setter:

function CombatOperation:noRepeats()
    self.repeats = false
end

function CombatOperation:init(attacker,defender, rng)
    self.attacker = attacker
    self.defender = defender
    self.repeats  = true
    self.random = rng or math.random
end

Commit: removed counter-attack logic.

In testing I find this bug again, picking up a Health: That’ll be an add to the crawl that doesn’t use the display operation. I would like to have the ability to cherry pick that but I do have the ability to read my own articles.

Provider:42: unexpected item in Provider array no op
stack traceback:
	[C]: in function 'assert'
	Provider:42: in method 'getItem'
	Floater:44: in method 'fetchMessage'
	Floater:55: in method 'increment'
	Floater:39: in method 'draw'
	GameRunner:201: in method 'drawMessages'
	GameRunner:163: in method 'draw'
	Main:30: in function 'draw'

The problem was here:

function Player:addPoints(kind, amount)
    local attr = self:pointsTable(kind)
    if attr then
        local current = self[attr]
        self[attr] = math.min(20,current + amount)
        self:doCrawl(kind, amount)
    end
end

function Player:doCrawl(kind, amount)
    local msg = string.format("+%d "..kind.."!!", amount)
    self.runner:addToCrawl({msg})
end

And GameRunner has this:

function GameRunner:addTextToCrawl(aString)
    self.cofloater:addItem(CombatOperation():display(aString))
end

So we use that:

function Player:doCrawl(kind, amount)
    local msg = string.format("+%d "..kind.."!!", amount)
    self.runner:addTextToCrawl(msg)
end

Now we can pick up health and other powerups without a problem. I also removed the unused Health-specific methods that were subsumed into Loot.

Commit: fixed problem in display when picking up Loot.

Now let’s assess where we are, and decide what to do.

Where Are We? What Shall We Do?

We’re on a good-as -any version, I’m pretty sure. We have one anomaly, which is that when we attack a monster, the monster may attack us right back. Here’s my explanation of how that happens.

Suppose we are side by side with a monster. We attempt to move onto his square. That calls the startActionWithMonster, and that creates a CombatOperation, and the CombatOp issues an attack into the Crawl. All that returns, and GameRunner then runs the monsters:

function Button:performCommand(player)
    player[self.name](player)
    player:turnComplete()
end

function Player:turnComplete()
    self.runner:turnComplete()
end

function GameRunner:turnComplete()
    --self.playerCanMove = false
    self:moveMonsters()
    --self.playerCanMove = true
end

The monster beside us, as yet unaware that we’ve attacked it, attacks us. A new CombatOperation is created, putting “Monster attacks Princess” into the queue. The Crawl now starts pulling from the queue, and because there are two CombatOp commands in there, does the one (for the Princess) and then the other (for the Monster).

This repeats until both CombatOps run down, alternating more or less sensible messages.

mutual

In the movie above we see the alternation of lines, and we see that the MurderHornet and the Princess manage to mutually kill each other. This should not happen. One or the other of them managed to strike after they were dead. Tricky, but not intended.

I did a few things yesterday, and at other points in the past, intended to deal with this.

  1. I put in a flag to stop anyone from moving while combat was going on.
  2. I cached the CombatOp when I created it (in GameRunner) and then simply refused to create another while there was one in existence.
  3. At some point, I created turnComplete and then didn’t use it, or remove it, in aid of the above.

Now, I do think we need a more rubust turn-taking mechanism. But first, I think we can do something better with combat that will help in this situation.

The only reason to put a new call to our CombatOp in the queue is if, ahead of it, there is an action that can change the state of things, that the following operation needs to know about.

At present, there are no such actions. We have no need to defer the full calculation of the combat round. I propose to fold it all back together. If we ever do need the deferral capability, we can always issue a deferred operation.

Along the way, I’m going to rename the class to CombatRound, which makes more sense. And we may find some simplification or things to remove from the OP class as well.

First the rename. All is well. Commit: rename CombatOperation to CombatRound.

Now let’s combine some bits. I’ll try to go seriously incrementally. Possibly learned my lesson? No, I’m sure I’ll fall back into my old ways soon enough. I’m never going to be perfect.

Here’s where the combat round will start:

function CombatRound:attack()
    if self.attacker:isDead() then return {} end
    local result = {}
    local msg = string.format("%s attacks %s!", self.attacker:name(), self.defender:name())
    table.insert(result, self:display(msg) )
    local cmd = { op="op", receiver=self, method="attemptHit" }
    table.insert(result,cmd)
    return result
end

It defers attemptHit, which will later do this:

function CombatRound:attemptHit()
    local result = {}
    local msg = string.format("%s whacks %s!", self.attacker:name(), self.defender:name())
    table.insert(result, self:display(msg) )
    local op = OP:op(self,"rollDamage")
    table.insert(result, op)
    return result
end

And then later still, rollDamage:

function CombatRound:rollDamage()
    local result = {}
    local damage = self.random(1,6)
    local op = { op="extern", receiver=self.defender, method="damageFrom", arg1=nil, arg2=damage }
    table.insert(result,op)
    table.insert(result, self:display(self.defender:name().." takes "..damage.." damage!"))
    return result
end

These can all be done together. There are no decisions being made beyond rolling damage. Let’s roll them in one at a time, however. So instead of deferring attemptHit, we’ll call it and append in its result.

I think this does it:

function CombatRound:attack()
    if self.attacker:isDead() then return {} end
    local result = {}
    local msg = string.format("%s attacks %s!", self.attacker:name(), self.defender:name())
    table.insert(result, self:display(msg) )
    local cmds = self:attemptHit()
    table.move(cmds,1,#cmds,result,#result+1)
    return result
end

Testing. That nearly works but the move is wrong. I did it wrong last time too. Should be:

    table.move(cmds,1,#cmds,#result+1, result)

Works fine.

Let’s roll up the next one. I choose to let attemptHit bring rollDamage up:

function CombatRound:attemptHit()
    local result = {}
    local msg = string.format("%s whacks %s!", self.attacker:name(), self.defender:name())
    table.insert(result, self:display(msg) )
    local op = OP:op(self,"rollDamage")
    table.insert(result, op)
    return result
end

That becomes:

function CombatRound:attemptHit()
    local result = {}
    local msg = string.format("%s whacks %s!", self.attacker:name(), self.defender:name())
    table.insert(result, self:display(msg) )
    local cmds = self:rollDamage()
    table.move(cmds,1,#cmds,#result+1, result)
    return result
end

I expect this to continue to work. It does, with the same caveat as before. Since the monster gets a turn immediately after the player has fired off her combat round, and before that round runs to completion, a monster that will have been killed can still issue an attack. But that’s not our concern inside the combat round. I think it’s doing its job.

Does rollDamage defer anything?

function CombatRound:rollDamage()
    local result = {}
    local damage = self.random(1,6)
    local op = { op="extern", receiver=self.defender, method="damageFrom", arg1=nil, arg2=damage }
    table.insert(result,op)
    table.insert(result, self:display(self.defender:name().." takes "..damage.." damage!"))
    return result
end

No, it’s just doing its thing happily.

We do have some tests that have started failing, however. Let’s see what’s up there. I suspect our tables have changed from the viewpoint of the tests, but let’s check the facts:

1: First Attack  -- OK
1: First Attack  -- OK
1: First Attack  -- Actual: display, Expected: op
1: First Attack  -- Actual: nil, Expected: CombatRound
1: First Attack  -- Actual: nil, Expected: attemptHit

The test says:

        _:test("First Attack", function()
            local result
            local i,r
            local player = FakeEntity("Princess")
            local monster = FakeEntity("Spider")
            local co = CombatRound(player, monster)
            result = co:attack()
            i,r = next(result,i)
            _:expect(r.op).is("display")
            _:expect(r.text).is("Princess attacks Spider!")
            i,r = next(result,i)
            _:expect(r.op).is("op")
            _:expect(r.receiver).is(co)
            _:expect(r.method).is("attemptHit")
        end)

That’s checking that we deferred the attemptHit, which we no longer do. Instead, the next result will be whatever attempted hit does. Let’s just not check that output, because we have a test for it. Remove the last four lines.

2: attempt hit  -- OK
2: attempt hit  -- OK
2: attempt hit  -- Actual: extern, Expected: op
2: attempt hit  -- Actual: table: 0x288f01f00, Expected: CombatRound
2: attempt hit  -- Actual: damageFrom, Expected: rollDamage

The test is:

        _:test("attempt hit", function()
            local result
            local i,r
            local player = FakeEntity("Princess")
            local monster = FakeEntity("Spider")
            local co = CombatRound(player, monster)
            result = co:attemptHit()
            i,r = next(result,i)
            _:expect(r.op).is("display")
            _:expect(r.text).is("Princess whacks Spider!")
            i,r = next(result,i)
            _:expect(r.op).is("op")
            _:expect(r.receiver).is(co)
            _:expect(r.method).is("rollDamage")
        end)

Same thing, remove last four lines. Tests are good.

This was evidence that what we did worked, though it would have been more clever of me to predict the results in those tests. And, arguably, there should be one test that checks the whole sequence, but I’m happy for now.

Commit: CombatRound enqueues all actions at once.

What’s Next?

I think the next issue in Combat is to deal with the turn-taking. The monsters can issue combat rounds right after the player does hers, and when they are dead, that looks a bit odd.

still dead

In the above movie, we see the Ankle Biter attack after it’s dead. It’s not to blame: it didn’t know it was dead.

Last time, I set things up so that a combat couldn’t be started if there was one running. Here, we don’t have that situation any more: a combat runs as a single function call. The issue is that the combat result will not be known until later, when the Crawl reveals the outcome.

This suggests that one possibility is that we should ask whether the crawl is running, and not allow the monsters to take their turn until the crawl has gone into its default mode. A reasonable equivalent would be to arrange for the Crawl to inform GameRunner whenever it starts defaulting. Either way, that could perhaps replace the current turnComplete that is triggered after a button or key is pressed.

But in that case, what would trigger us into turn complete if all we did was move from one innocuous tile to another? We don’t enqueue anything there.

Let’s look more deeply at how the turn logic works now:

We have the player checking to see if it is its turn:

function Button:shouldAccept(aTouch, player)
    return aTouch.state == BEGAN and player:itsOurTurn() and self:mine(aTouch)
end

function Player:keyPress(key)
    if not self:itsOurTurn() then return false end
    self:executeKey(key)
    self:turnComplete()
end

function Player:itsOurTurn()
    return self.runner:itsPlayerTurn()
end

function GameRunner:itsPlayerTurn()
    return self.playerCanMove
end

function GameRunner:stopFloater()
    self.playerCanMove = true
end

function GameRunner:startFloater()
    self.playerCanMove = false
end

function Floater:startCrawl(listener)
    self.listener = listener or FloaterNullListener()
    self.yOff = self.yOffsetStart
    self.buffer = {}
    self.listener:startFloater()
    self:fetchMessage()
end

function Floater:startCrawl(listener)
    self.listener = listener or FloaterNullListener()
    self.yOff = self.yOffsetStart
    self.buffer = {}
    self.listener:startFloater()
    self:fetchMessage()
end

function Floater:runCrawl(array, listener)
    self.provider:addItems(array)
    self:startCrawl(listener)
end

function GameRunner:runCrawl(array)
    self.cofloater:runCrawl(array)
end

function GameRunner:runBlockingCrawl(array)
    self.cofloater:addItems(array)
end

I think a lot of this is obsolete and should have been removed. Bad code, not debt, by the way.

No one calls runBlockingCrawl. Remove, test, commit.

I don’t see any calls to GameRunner:runCrawl. Remove, test … wrong. Create level calls it. Put it back.

I want that method gone. No one calls it from outside. Inline it. All good. Commit: removed runCrawl from GameRunner.

I think this means that no one is calling the crawl with a listener. However, that seems to me to be a powerful feature, and we should probably continue to have it. So I’m not going to remove it. I’m kind of thinking we’ll use it when we get to improving the turn logic a bit.

Note what the listener stuff does. When the queue runs dry, it sends stopFloater to its listener:

function Floater:increment(n)
    self.yOff = self.yOff + (n or self:adjustedIncrement())
    if self:linesToDisplay() > self.lineCount then
        table.remove(self.buffer,1)
        self.yOff = self.yOff - self.lineSize
    end
    if #self.buffer < self:linesToDisplay() then
        self:fetchMessage()
    end
    if #self.buffer == 0 then
        self.listener:stopFloater()
    end
end

It presently only sends startFloater on startCrawl, which only happens once, since the crawl runs continually now, saying nothing. But we do have these methods:

function Floater:addItem(item)
    self.provider:addItem(item)
end

function Floater:addItems(array)
    self.provider:addItems(array)
end

Those could trigger startFloater and that could be useful. I’ll leave this as is for now.

Time for a break, and then some. I’ll come back to sum up, but we’re probably done for today.

Summary

So where are we? I’ve not yet solved the problem of combat. It’s still possible for a monster to attack, who will have been dead1 at the time he attacks. I have solved the interleaving of two attack crawls, and the code is far more clean than it was. And some other things have been cleaned up and simplified. There’s a net code reduction of about 40 lines, which is nice.

I do have some ideas about resolving the occasional extra battle, when a dead thing attacks. I think the basic notion should be to enforce turn-taking more effectively. The trick will be to be sure that we catch all the ways that a player’s turn can end and cover them, because if we miss one, the game stops. I think the monsters may be easier, as they are completely computer-controlled.

I think we will need to cover two kinds of end to the player’s turn, simple moves or other actions, and the end of combat. I suppose it may turn out that the monsters have the same issues exactly, except that their actions won’t take ages of computer time like human ones do.

I’m fairly sure that a single combat round can always be done in one go, and if it can’t, we still have the ability to stuff a deferred command into the queue. As I mentioned, this might come up if we have multiple attacks from one entity in a single round. Right now, we’re not planning on that.

Bigger Picture

But what about the bigger picture here? While I haven’t really wasted 40 Ron-days working on this, I’ve probably spent 20. That’s a person-week or more. Of course I spend at least as much elapsed time writing the articles as I do coding.2 But is the design going to bear weight, and could we have avoided some of the rework?

We were talking last night in the Zoom Ensemble about a fear many programmers have of wasting a keystroke. They feel that if they have to redo something, or refactor something, it’s a failure. So they resist changing the code.

I’m reminded of the story about making pots, from Art & Fear, by Bayles and Orland.

The ceramics teacher announced on opening day that he was dividing the class into two groups. All those on the left side of the studio, he said, would be graded solely on the quantity of work they produced, all those on the right solely on its quality. His procedure was simple: on the final day of class he would bring in his bathroom scales and weigh the work of the “quantity” group: fifty pound of pots rated an “A,” forty pounds a “B,” and so on. Those being graded on “quality,” however, needed to produce only one pot–albeit a perfect one–to get an “A.”

Well, came grading time and a curious fact emerged: the works of highest quality were all produced by the group being graded for quantity. It seems that while the “quantity” group was busily churning out piles of work–and learning from their mistakes–the “quality” group had sat theorizing about perfection, and in the end had little more to show for their efforts than grandiose theories and a pile of dead clay.

In this little program, I’ve tried several ways of doing combat, and learned from each one. I’ve learned about how to do combat, but also learned about other things. I’ve learned some very interesting deep details about Lua, and had some this-century practice with coroutines.

As an aside, Bill said last night, when I was describing the Crawl and associated concerns, that it sure sounded like a problem for coroutines to him. I think we’ve found, so far, that coroutines are a bit too much for the problem, but, who knows, we might find that we need them.

The main point is, in a real ongoing product development, we’d better be learning all the time, better and better ways of doing what we’re called upon to do. So doing some experiments, spikes, as they’re called, is a key aspect of doing the work. Sometimes we do them separately from the main code, sometimes we do them as part of it. Sometimes we throw them out immediately, and sometimes we retain them as better than what we had, until something even better comes along.

As GeePaw Hill puts it, programming is about changing code. It’s what we do. We should always feel good about making the code better.

Today, I’ve made things more understandable, better organized, and removed a few defects and anomalies. That’s the kind of day that I like.

Next time, we’ll work on keeping the dead monsters calm. See you then, I hope!


D2.zip


  1. You don’t really get to use future perfect tense all that often. This article has been a treasure, if only for that. 

  2. On days like yesterday, we’re fortunate that he doesn’t code more!