Wow, he’s really dragging this one out, isn’t he? Let’s do some more Combat, round this out a bit. Later: Things go very wrong. This is a debacle.

I’m starting to think that using a raw table as our operation codes isn’t ideal. We have to say things like this:

    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)

That’s pretty awkward to type, and not very communicative. I think we need a little class to do this service. Our tests don’t use this syntax at all, the creation of these tables is all in our code.

Since Lua classes are just tables, we can probably do this fairly incrementally, though I would like to git ‘er done all at one go this morning. Our tables have these elements:

  • op: the opcode
  • text: used only in op display.
  • receiver: receiver of an extern op
  • method: method to call from an extern
  • arg1,arg2: args to pass to an extern

I think that’s all there is. I want to wind up with a simple helper class. I’ll just pick places in the code where we create a table, and replace that code with something better, then make it work.

We have a helper method for display. Let’s start there:

function CombatOperation:display(aString)
    return {op="display", text=aString}
end

Let’s imagine a class OP, with factory methods. So we might say this:

function CombatOperation:display(aString)
    return { OP:display(aString) }
end

And that is enough to cause me to code this:

OP = class()

function OP:display(aString)
    local op = OP("display")
    op.text = aString
    return op
end

function OP:init(op, receiver, method, arg1, arg2)
    self.op = op
    self.receiver = receiver
    self.method = method
    self.arg1 = arg1
    self.arg2 = arg2
end

Actually, I went beyond what is necessary for this “test” but it seemed sensible and we’ll get there soon.

Does this work? Let’s test. Well, no.

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:84: in method 'startCrawl'
	Floater:72: in method 'runCrawl'
	GameRunner:295: in method 'runCrawl'
	GameRunner:93: in method 'createLevel'
	Main:18: in function 'setup'

I’m not at all sure why that didn’t work, so I’ll write a little test. This may help me get into the swing of testing these supposedly trivial creation methods. In fact we have this test already:

        _:test("display", function()
            local player = FakeEntity("Princess")
            local monster = FakeEntity("Spider")
            local co = CombatOperation(player, monster)
            local op = co:display("This text")
            _:expect(op.op).is("display")
            _:expect(op.text).is("This text")
        end)

That test is failing, saying:

4: display  -- Actual: nil, Expected: display
4: display  -- Actual: nil, Expected: display

Oh. I didn’t remove the brackets in the operation. My bad. The reason? Lack of clarity in the code, and my mind, as to when we return a table, and when we return a table of tables.

This should fix us up:

function CombatOperation:display(aString)
    return OP:display(aString)
end

Yes, all better now. Let’s do another.

function CombatOperation: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", receiver=self, method="rollDamage" }
    table.insert(result, op)
    return result
end

We’ll do that latter entry.

function CombatOperation: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,"rollDamaage")
    table.insert(result, op)
    return result
end

I’m not feeling comfortable here. I think this will be better, but it doesn’t feel quite right. There may be ideas still forming here. But this does feel like a reasonable step, so we’ll press on.

function OP:op(combatOperation, method)
    return OP("op", combatOperation, method)
end

I expect this to work. It would probably work better without the two a’s in rollDamaage. A test found that.

Otherwise things work fine. I do notice an anomaly that I didn’t notice yesterday. Now that monsters can attack the princess as well as princess attacking monsters, it’s possible that she’ll initiate an attack, which will start the attack sequence, but then after the player moves, monsters can move, and one might attack her. We have removed the motion locking upon start of combat–I think–and we need to deal somehow with the situation.

Not now, though. We’re on a mission.

Truth is, these changes are entirely incremental. We could commit this work: it’s quite shippable, and move to the other concern, which is probably a higher priority than improving this code. So, yes, we’ll go after the simultaneous combat problem.

Commit: New OP class used for display and rollDamage.

Simultaneous Combat

The thinking here is nearly good but things start to go wrong.

There are at least two ways we might get into a situation where two CombatOperations are trying to run at the same time. One could occur as we just saw. Since monsters get a move turn after a player move. If the player attacks in her move, an adjacent monster could decide to attack. I’m surprised that I didn’t see that happening yesterday, in fact.

Another possibility is that there might be two monsters a square apart, and the player might move between them. In this case, both might attack. This could even happen with three monsters at once.

A third possibility, as yet unimplemented but contemplated, would be that monsters might move around while combat is going on, and new monsters might come up and join in. This would make for interesting game play.

And, I suppose, we could at least imagine that there are NPCs that are on the princess’s side. They might follow her around, and might pile on in the case of a battle. I really don’t think we’ll do that, but it might be fun and might make the game more enjoyable.

So, bottom line is, entities might want to join the battle. The current case is that the joining entity is already in the battle and just doesn’t know it. The other cases would entail additional ones joining in.

I see two things we might do.

First, we could just turn off motion, as we did in earlier versions, as soon as a battle starts. That would limit the combat to the two involved, and neither the princess nor the monster would try to get further involved.

The other option is more challenging. We could arrange the CombatOperation to allow not just one entity as attacker or defender, but more than one: an array or collection. How would that behave? When there was a collection of attackers, we’d probably want each one’s attack to go separately through the whole flow, attack, attemptHit, rollDamage. Then the next attacker would go. On the defense side, we’d probably want to allow the princess to choose which monster to attack.

Just glancing at the code that ends a combat cycle now, we see this:

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

Instead of just automatically doing counterAttack, we’d check to see if there was another attacker in the batch, and if so, initiate a new attack on their behalf.

The “big switch” to stop all action may be necessary, but it’s a very big hammer. Let’s see if we can do something a bit in the middle. Let’s see how combat gets started:

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

We have some duplication here anyway. If we were to send a message to the runner to start the combat, then the runner could know whether there’s already combat running and act appropriately.

Let’s start there:

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

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

function GameRunner:initiateCombat(attacker, defender)
    if defender:isDead() or attacker:isDead() then return end
    self.combat = CombatOperation(attacker,defender)
    self:addToCrawl(self.combat:attack())
end

This should leave us exactly where we were. Let’s check.

It does, once I correct the initiateCombat to say self:addToCrawl, not self.runner:addToCrawl.

Still OK here, but about to go off the rails.

Now let’s init the combat field in GameRunner, and use it in initiateCombat. We’ll have to find a way to clear it as well. I’ve inited it to nil in GameRunner:init and will spare you that batch of code. GameRunner is getting rather overly large, though. Now how to turn the combat field back to nil when the Combat is over?

I think we can do that in counterAttack:

function CombatOperation:counterAttack(result)
    if not self.repeats then return result end
    local co = CombatOperation(self.defender, self.attacker)
    co:noRepeats()
    local ops = co:attack()
    table.move(ops,1,#ops, #result+1,result)
end

We should probably add in a new operation for this, endCombat. How about this:

function CombatOperation:counterAttack(result)
    if not self.repeats then 
        table.insert(result, OP:extern(self.runner(), "endCombat"))
        return result 
    end
    local co = CombatOperation(self.defender, self.attacker)
    co:noRepeats()
    local ops = co:attack()
    table.move(ops,1,#ops, #result+1,result)
end

I have to have a method for runner, not because I don’t know how to get it, but to reflect the horrid way I have to get it:

function CombatOperation:runner()
    return self.attacker.runner
end

And now I want endCombat in GameRunner:

function GameRunner:endCombat()
    self.combat = nil
end

This should still have no different effect. I think I’ll toss a print into endCombat just to feel more sure.

So that wasn’t necessary:

TestCombatOperation:171: attempt to index a nil value (local 'self')
stack traceback:
	TestCombatOperation:171: in field 'runner'
	TestCombatOperation:151: in method 'counterAttack'
	TestCombatOperation:166: in field '?'
	Provider:28: in method 'execute'
	Provider:40: in method 'getItem'
	Floater:44: in method 'fetchMessage'
	Floater:55: in method 'increment'
	Floater:39: in method 'draw'
	GameRunner:202: in method 'drawMessages'
	GameRunner:164: in method 'draw'
	Main:30: in function 'draw'

Hm what’s this?

function CombatOperation:runner()
    return self.attacker.runner
end

I’ll bet I have a dot where I need a colon:

function CombatOperation:counterAttack(result)
    if not self.repeats then 
        table.insert(result, OP:extern(self.runner(), "endCombat"))
        return result 
    end
    local co = CombatOperation(self.defender, self.attacker)
    co:noRepeats()
    local ops = co:attack()
    table.move(ops,1,#ops, #result+1,result)
end

Sure do.

function CombatOperation:counterAttack(result)
    if not self.repeats then 
        table.insert(result, OP:extern(self:runner(), "endCombat"))
        return result 
    end
    local co = CombatOperation(self.defender, self.attacker)
    co:noRepeats()
    local ops = co:attack()
    table.move(ops,1,#ops, #result+1,result)
end

Now then. And …

TestCombatOperation:151: attempt to call a nil value (method 'extern')
stack traceback:
	TestCombatOperation:151: in method 'counterAttack'
	TestCombatOperation:166: in field '?'
	Provider:28: in method 'execute'
	Provider:40: in method 'getItem'
	Floater:44: in method 'fetchMessage'
	Floater:55: in method 'increment'
	Floater:39: in method 'draw'
	GameRunner:202: in method 'drawMessages'
	GameRunner:164: in method 'draw'
	Main:30: in function 'draw'

Someone didn’t implement extern.

function OP:extern(receiver, method, arg1, arg2)
    return OP("extern", receiver, method, arg1, arg2)
end

I’m right to feel fast and loose. Wrong to feel close.

I’m feeling fast and loose here. This isn’t good, but I think we’re close to this working. I should calm down or hand the keyboard to my pair, but as fine as my cat is, she’s a terrible typist. At last, the “endCombat` does print. Now let’s use the combat field. We have this:

function GameRunner:initiateCombat(attacker, defender)
    if defender:isDead() or attacker:isDead() then return end
    self.combat = CombatOperation(attacker,defender)
    self:addToCrawl(self.combat:attack())
end

One thing we could do is simply ignore the call if there is already a combat op. Another would be to check whether the entrant is already in there and if not, add them. I think that’s for later. For now, we’ll just decline to create a combat op if there is one.

function GameRunner:initiateCombat(attacker, defender)
    if self.combat or defender:isDead() or attacker:isDead() then return end
    self.combat = CombatOperation(attacker,defender)
    self:addToCrawl(self.combat:attack())
end

I expect to see more orderly combat now.

Combat is NOT fine.

Combat is fine, but I have encountered another problem, stepping on a powerup:

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:202: in method 'drawMessages'
	GameRunner:164: in method 'draw'
	Main:30: in function 'draw'

This will be a leftover from converting to the ore robust op format. This bug will have been in there for a day or more.

Bad, Ron, bad. Let’s fix.

That must be in Loot.

function Loot:actionWith(aPlayer)
    self.tile:removeContents(self)
    aPlayer:addPoints(self.kind, math.random(self.min, self.max))
end

So …

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

I also find this:

function Player:addHealthPoints(points)
    local msg = string.format("+%d Health!!", points)
    self.healthPoints = math.min(20, self.healthPoints + points)
    self.runner:addToCrawl({msg})
end

Seems to me that runner has a more useful method for us.

Yes:

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

We can call that from those guys above:

function Player:doCrawl(kind, amount)
    local msg = string.format("+%d "..kind.."!!", amount)
    self.runner:addTextToCrawl(msg)
end
function Player:addHealthPoints(points)
    local msg = string.format("+%d Health!!", points)
    self.healthPoints = math.min(20, self.healthPoints + points)
    self.runner:addTextToCrawl(msg)
end

But is this last thing even used? Didn’t it get subsumed into Loot? Or do we still use the Health object? I don’t see it. Let’s remove those.

The powerups work correctly, but I’ve seen something else odd. Let me see if I can get a movie for you.

dead

As we see above, the ghost attacks before it is found to be dead, dealing damage after it is doubly deceased.

It seems that the counter-attack was filed before we discovered that the ghost was dead. The problem is repeatable. We have a bug in our combat compiler somewhere.

Let’s review the object. We first saw her whack the ghost:

function CombatOperation: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

She’ll then roll damage:

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

And then we go to counterAttack … but at this point the creature isn’t dead. It won’t be dead until the damage line is actually pulled.

This suggests that we need to condition attack to check for defender dead.

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

Here again, some of the thinking is valuable. But we’re off the road and not really coming back.

But wait. What we’re seeing is that the attacker, the ghost, should already be dead by now. But the death still hasn’t come up in the newsreel, so we enqueue the attacks display, and then the attemptHit operation, and so on.

One solution, of course, is to predict the future and determine that the defender will be dead before we even do the counter attack. That requires duplication of effort, or some other yucchy thing. Might be an opportunity for a “future” object, but I really don’t want to go there.

Another possibility is not to call counterAttack directly, but instead to enqueue a counter attack operation. That would fall in behind the damage call, and therefore the check for life should be timely.

Let’s try that. Check this again:

function CombatOperation:counterAttack(result)
    if not self.repeats then 
        table.insert(result, OP:extern(self:runner(), "endCombat"))
        return result 
    end
    local co = CombatOperation(self.defender, self.attacker)
    co:noRepeats()
    local ops = co:attack()
    table.move(ops,1,#ops, #result+1,result)
end

This is A problem but not THE problem.

This code creates a different combat operation instance from the one that the GameRunner has. We really shouldn’t do that, it can only lead to trouble. But I have to confess here, I’ve lost track of who’s holding on to the CombatOperation. Let’s see. An OP that refers to the CombatOperation has it in receiver, such as:

    local cmd = { op="op", receiver=self, method="attemptHit" }

So that will maintain continuity up until this counter attack stuff. We need to reverse the parameters and set the flag, but continue to use the same CombatOperation, lest the GameRunner and combat get out of sync.

And we want to enqueue the operation to allow for the defender to meet their potential demise.

Fool!

Wow, this is tricky, isn’t it? I think the scheme is still bearing the necessary weight, but I’m having trouble keeping the past, present, and future in mind. Something simpler needs to evolve from this.

But for now, our purpose is just to enqueue the reversal and attack again. First, we’ll use ourself here, not a new CO:

function CombatOperation:counterAttack(result)
    if not self.repeats then 
        table.insert(result, OP:extern(self:runner(), "endCombat"))
        return result 
    end
    local co = CombatOperation(self.defender, self.attacker)
    co:noRepeats()
    local ops = co:attack()
    table.move(ops,1,#ops, #result+1,result)
end
function CombatOperation:counterAttack(result)
    if not self.repeats then 
        table.insert(result, OP:extern(self:runner(), "endCombat"))
        return result 
    end
    self.attacker,self.defender = self.defender, self.attacker
    self:noRepeats()
    local ops = self:attack()
    table.move(ops,1,#ops, #result+1,result)
end

This should elicit no change at all.

Ah. In a moment of clarity, I see the issue a bit more, well, clearly.

How many clues does this man need that he’s burying himself?

We have filed an extern damage call into the queue. When that damage call is pulled, the monster will be damaged, found to be dead, and a death notice will be filed. Anything we put into the queue now will be pulled before the death notice comes out. And anything that thing does will be pulled before the death notice comes out.

Essentially, we cannot even enqueue a counter-attack. or anything else that cares about the round’s result, until the queue empties.

The counter round has to be a separate combat operation. What are we to do?

One possibility comes to mind: Give GameRunner a stack of CombatOperations. When one ends, it starts the next. My main objection to this idea is that we already have one queue going, the Provider, and it seems to me that another can only lead to trouble.

Another thought occurred to me. What if when an monster takes damage, it were to send a message at that point, requesting a counter-attack, in the event that it survives the blow? Or what if it just starts a new attack?

Wouldn’t that interfere with our endCombat work, though? In fact, that’s currently being done too early, isn’t it? Combat isn’t over until the damage is done.

Arrgh. This isn’t the end of the world, but it isn’t good. In principle, the endCombat should be filed at the time damage is actually applied, not before.

At least he didn’t try this.

Yet another possibility comes to mind. We could change our combat approach to a more state-driven one. Imagine a CombatStateMachine. When a state is entered, it emits whatever operations it needs to and then stops. (But the CombatStateMachine is still “known” to GameRunner (or maybe pushed down to Provider). When Provider runs empty, if there’s a state machine available, the machine is told to go to its next state, and it again emits some operations.

This would mean Yet Another Implementation Of Combat, but it wouldn’t be terribly different from the existing one, except with a state machine on top.

Another thought. Provider is looking at everything in the queue already:

function Provider:getItem()
    if #self.items < 1 then return self.default end
    local item = table.remove(self.items,1)
    if item.op == "display" then 
        return item.text
    elseif item.op == "extern" then
        self:execute(item)
    elseif item.op == "op" then
        self:addItems(self:execute(item))
    else
        assert(false, "unexpected item in Provider array "..(item.op or "no op"))
    end
    return self:getItem()
end

function Provider:execute(command)
    local receiver = command.receiver
    local method = command.method
    local arg1 = command.arg1
    local arg2 = command.arg2
    local t = receiver[method](receiver, arg1, arg2)
    return t
end

Good idea, actually. But we’re out of control.

And Provider “knows” the difference between the opcodes. It is essentially the machine that executes the OPs. What if we had an OP that caused Provider to save it away, and then to execute the OP when Provider would otherwise return a default?

This might be a new opcode “deferred”, otherwise the same as “op”. I think this could work.

I’m just going to put it in. I don’t see a test for it, and I’m just hanging on by my fingernails here.

function Provider:init(default)
    self.default = default or "default"
    self.items = {}
    self.deferred = nil
end

Then, first, this, where we save the op:

function Provider:getItem()
    if #self.items < 1 then return self.default end
    local item = table.remove(self.items,1)
    if item.op == "display" then 
        return item.text
    elseif item.op == "extern" then
        self:execute(item)
    elseif item.op == "op" then
        self:addItems(self:execute(item))
    elseif item.op == "deferred" then
        assert(self.deferred==nil, "Cannot defer two ops")
        self.deferred = op
    else
        assert(false, "unexpected item in Provider array "..(item.op or "no op"))
    end
    return self:getItem()
end

Then to use it if we have it:

function Provider:getItem()
    if #self.items < 1 and self.deferred then
        self:addItems(self:execute(self.deferred))
        self.deferred = nil
    end
    if #self.items < 1 then return self.default end
    local item = table.remove(self.items,1)
    if item.op == "display" then 
        return item.text
    elseif item.op == "extern" then
        self:execute(item)
    elseif item.op == "op" then
        self:addItems(self:execute(item))
    elseif item.op == "deferred" then
        assert(self.deferred==nil, "Cannot defer two ops")
        self.deferred = op
    else
        assert(false, "unexpected item in Provider array "..(item.op or "no op"))
    end
    return self:getItem()
end

When you feel like this, revert!

I almost think this could work. I’m far from confident, however, this is some pretty intense juggling of ideas. Now to defer the counterattack. We have this:

function CombatOperation:counterAttack(result)
    if not self.repeats then 
        table.insert(result, OP:extern(self:runner(), "endCombat"))
        return result 
    end
    self.attacker,self.defender = self.defender, self.attacker
    self:noRepeats()
    local ops = self:attack()
    table.move(ops,1,#ops, #result+1,result)
end

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

I think that both the endCombat shown above and the attack should be deferred. And attack is a multi-operation command, so we need to defer a new command that will do the reversal and the attack.

I’m going to try this:

function CombatOperation:counterAttack(result)
    if not self.repeats then 
        table.insert(result, OP:defer(self:runner(), "endCombat"))
        return result 
    end
    self:noRepeats()
    local deferredOP = OP:defer(self,"flipAndAttack")
    table.insert(result, deferredOp)
end

And we need defer:

function OP:defer(receiver,method,arg1, arg2)
    return OP("deferred", receiver, method, arg1, arg2)
end

Flailing.

This could work. I honestly don’t know but I feel somewhat optimistic.

It is but to try. Well, the first battle works, but neither the princess nor a monster can start a second one. Which makes me think that the deferred ‘endCombat` never happened.

I need a test where I don’t kill the creature outright, to see if it gets its counter.

It does not. Let’s look deeper. I’ll instrument Provider regarding deferred.

Ah, wait. I can’t put it away marked deferred. It has to be marked ``extern` or it won’t execute later.

    elseif item.op == "deferred" then
        assert(self.deferred==nil, "Cannot defer two ops")
        op.op = "extern"
        self.deferred = op

Try try again. Still same effect. Instrument as intended:

I’m wrong about changing the opcode. Since the deferred execution doesn’t check, it’ll just execute fine. Anyway, now I have some prints in.

I’m not seeing the deferred operation going in. Did I somehow fail to file it?

As far as I can see, the deferred message, “flipAndAttack” is not getting into the Provider’s queue. Let’s look at the code that I thought did the job:

function CombatOperation:counterAttack(result)
    print("counterAttack", self.repeats)
    if not self.repeats then 
        print("counterAttack files endCombat deferred")
        table.insert(result, OP:defer(self:runner(), "endCombat"))
        return result 
    end
    self:noRepeats()
    local deferredOP = OP:defer(self,"flipAndAttack")
    print("deferring ", deferredOP.method)
    table.insert(result, deferredOp)
end

Do you see it? One has P, the other has p.1

Ow. Anyway …

    local deferred = OP:defer(self,"flipAndAttack")
    print("deferring ", deferred.method)
    table.insert(result, deferred)

Now does it work? No, we get this:

Provider:50: attempt to index a nil value (global 'op')
stack traceback:
	Provider:50: in method 'getItem'
	Floater:44: in method 'fetchMessage'
	Floater:55: in method 'increment'
	Floater:39: in method 'draw'
	GameRunner:202: in method 'drawMessages'
	GameRunner:164: in method 'draw'
	Main:30: in function 'draw'

That’ll be a simple typo:

    elseif item.op == "deferred" then
        assert(self.deferred==nil, "Cannot defer two ops")
        print("deferring ", op.method)
        op.op = "extern"
        self.deferred = op
    else

Too fancy by far. I don’t need to change the opcode, and that’t not how to do that or the print.

    elseif item.op == "deferred" then
        assert(self.deferred==nil, "Cannot defer two ops")
        print("deferring ", item.method)
        self.deferred = op

This is progress. Too little, too late.

I am encouraged, we were going down the expected path. However, I do get the deferring message, but then nothing about undeferring it.

The sequence I see is:

  • executing attemptHit
  • executing rollDamage
  • counterAttack true
  • CO deferring flipAndAttack
  • executing damageFrom
  • Provider deferring flipAndAttack.

What I would have liked to see was Provider undeferring as well.

Sheesh.

    elseif item.op == "deferred" then
        assert(self.deferred==nil, "Cannot defer two ops")
        print("Provider deferring ", item.method)
        self.deferred = op

There is no op. op is nil. I meant item. Now I get a new error. This is progress, of a sort.

Provider:30: attempt to call a nil value (field '?')
stack traceback:
	Provider:30: in method 'execute'
	Provider:40: in method 'getItem'
	Floater:44: in method 'fetchMessage'
	Floater:55: in method 'increment'
	Floater:39: in method 'draw'
	GameRunner:202: in method 'drawMessages'
	GameRunner:164: in method 'draw'
	Main:30: in function 'draw'

This could be more clear. I know what it is, I think. Let’s make it more clear:

Provider:31: receiver doesn't know flipAndAttack
stack traceback:
	[C]: in function 'assert'
	Provider:31: in method 'execute'
	Provider:42: in method 'getItem'
	Floater:44: in method 'fetchMessage'
	Floater:55: in method 'increment'
	Floater:39: in method 'draw'
	GameRunner:202: in method 'drawMessages'
	GameRunner:164: in method 'draw'
	Main:30: in function 'draw'

There we go, something I actually expected. Now to build flipAndAttack.

Provider:18: did not get a table in addItems
stack traceback:
	[C]: in function 'assert'
	Provider:18: in method 'addItems'
	Provider:42: in method 'getItem'
	Floater:44: in method 'fetchMessage'
	Floater:55: in method 'increment'
	Floater:39: in method 'draw'
	GameRunner:202: in method 'drawMessages'
	GameRunner:164: in method 'draw'
	Main:30: in function 'draw'

Finally, he wakes up to reality.

Arrgh, one more thing after another. I’m not distinguishing between deferred combat ops and deferred regular methods. Remember that combat ops guarantee to return a table and regular methods do not (although they might file an entry back through GameRunner.

I can “readily” patch this. But it’s too much, isn’t it? If you have read to this point, haven’t you already shouted “revert, you’re in a hole and still digging”?

I’d kind of like to cherry-pick some of what’s done. We did that change such that we always use the same CombatOperation, and the GameRunner knows not to start a second one. That actually worked. I think.

No, the answer is to revert, toss in the towel, declare everything since 9:40 AM a “learning experience”.

So be it.

I’ll go back and mark the article where it started to go wrong. Then let’s sum up.

Summary of the Debacle

There were some good ideas along the way here, and even some good fixes and improvements. Because I didn’t commit them, they are all lost, tears in the rain. Ah well, no matter, we can do it again tomorrow, better.

The larger-scale issue, simply stated, is that the design goal of keeping the visual effects in time with the crawl, while calculating combat more or less in a batch, is turning out to be difficult to meet. There’s really only one issue, but it’s a challenging one.

There are operations being done, specifically applying damage and resulting death, at the time the crawl finally pulls the damage messages. Until the crawl sees those messages, the entity does not appear to be damaged or dead. Meanwhile, combat decisions are being made. They are being made on information which will, in the future, turn out not to be true. They look at the entity and see it alive, when “in fact” it’s going to be dead at the time their decision takes effect.

The simple case is that a monster that is slain by the princess can appear to attack even though it is dead. This is not a good look.

Despite all the things I like about the present CombatOperation / Provider connection, and the new little OP object helper, this is a serious flaw and while there may be a simple and obvious repair for it, I’ve certainly failed so far to find it.

Some days the bear bites you. Today, it bites me.

We’ll go after this again with a fresh outlook, in the next article. I hope you’ve learned an important lesson from this one, which is to shout “Revert, you fool!” much sooner

See you next time!


  1. And thus makes she her great P’s.