Let’s start on our new combat logic. But first, those failing tests …

The Floater was changed not to use coroutines, which has pretty well wiped out the tests. Let’s see whether to fix them or remove them.

2: floater initialize -- attempt to index a function value
3: floater pulls messages appropriately -- attempt to index a function value

The tests are:

        _:test("floater initialize", function()
            local fl = Floater(nil, 50, 25, 4)
            fl:runCrawl(msg)
            _:expect(fl.yOffsetStart).is(50)
            _:expect(fl.lineSize).is(25)
            _:expect(fl.lineCount).is(4)
            _:expect(fl:linesToDisplay()).is(1)
            _:expect(fl:yOffset()).is(50)
            fl:increment(25)
            _:expect(fl:yOffset()).is(75)
            _:expect(fl:linesToDisplay()).is(2)
        end)
        
        _:test("floater pulls messages appropriately", function()
            local fl = Floater(nil,50,25,4)
            fl:runCrawl(msg)
            _:expect(#fl.buffer).is(1)
            _:expect(fl.buffer[1]).is("Message 1")
            fl:increment(24)
            _:expect(#fl.buffer).is(1)
            _:expect(fl.buffer[1]).is("Message 1")
            fl:increment()
            _:expect(#fl.buffer).is(2)
            _:expect(fl.buffer[2]).is("Message 2")
            fl:increment(25)
            _:expect(fl.buffer[3]).is("Message 3")
            fl:increment(25)
            _:expect(fl.buffer[4]).is("Message 4")
            
            _:expect(fl:yOffset()).is(125.5)
            fl:increment(25)
            _:expect(fl:yOffset()).is(125.5)
            _:expect(#fl.buffer).is(4)
            _:expect(fl.buffer[1]).is("Message 2")
            _:expect(fl.buffer[4]).is("Message 5")
            
            fl:increment(25)
            _:expect(fl:yOffset()).is(125.5)
            _:expect(#fl.buffer).is(3)
            _:expect(fl.buffer[1]).is("Message 3")
            
            fl:increment(25)
            _:expect(fl:yOffset()).is(125.5)
            _:expect(#fl.buffer).is(2)
            _:expect(fl.buffer[1]).is("Message 4")
            
            fl:increment(25)
            _:expect(fl:yOffset()).is(125.5)
            _:expect(#fl.buffer).is(1)
            _:expect(fl.buffer[1]).is("Message 5")
            
            fl:increment(25)
            _:expect(fl:yOffset()).is(125.5)
            _:expect(#fl.buffer).is(0)
            
        end)

My initial reaction to this is “ARRGH!”.

Now, let’s keep in mind that TDD tests serve two purposes. The primary purpose is that they help us focus on small steps to getting our code to do what it wants. The test says “let’s do this next”, and when we’ve done it, the test says “OK”. The second purpose is why we keep them: when we inevitably change the code, the TDD tests will generally alert us if we’ve broken something. They act as a sort of “contract”, saying what we’ve agreed the code will do, and they check to see that it still does.

Both of these purposes have value. To me, the first purpose is more valuable, because it helps me move in tiny steps toward a goal that comes clear as I work. The second is valuable as well, but sometimes, like now, my inclination is to say, well, these guys have served their purpose, thank them, and send them on their way.

But let’s at least look. The floater is pretty complicated. I was looking at it yesterday, as I did the new version that doesn’t rely on coroutines, and the geometry and such is rather tricky. So let’s see what’s up. Well, the whole premise of the tests is to supply a coroutine, but let’s see what happens if we provide the expected calling sequence.

local lines = { "Message 1", "Message 2", "Message 3", "Message 4", "Message 5" }

        _:test("floater initialize", function()
            local fl = Floater(nil, 50, 25, 4)
            fl:runCrawl(lines)
            _:expect(fl.yOffsetStart).is(50)
            _:expect(fl.lineSize).is(25)
            _:expect(fl.lineCount).is(4)
            _:expect(fl:linesToDisplay()).is(1)
            _:expect(fl:yOffset()).is(50)
            fl:increment(25)
            _:expect(fl:yOffset()).is(75)
            _:expect(fl:linesToDisplay()).is(2)
        end)

I just created an array of the expected messages and pass it in. What does that test do now?

2: floater initialize -- attempt to index a nil value

Well, that’s different, it used to be a function value now it’s a nil. And there are no “OK” messages before it coms out. Is that even the new creation format? Yes, it is. I wonder what’s breaking. Putting an extra check ahead of the runCrawl tells me that yes, we did manage to get through the creation, and that we exploded in the runCrawl.

Ah. I’ve added the lines in a place where CodeaUnit can’t find them. It does all kinds of strange tab-reading things in order to give it its ability to sniff out functions named testThisAndThat.

I’ll just move that inside the test. The test now runs correctly. Yay!

I’ll try the same thing on the other test.

I get 14 OKs, followed by:

3: floater pulls messages appropriately  -- Actual: 4, Expected: 3
OK
OK
3: floater pulls messages appropriately  -- Actual: 4, Expected: 2
OK
OK
3: floater pulls messages appropriately  -- Actual: 4, Expected: 1
OK
OK
3: floater pulls messages appropriately  -- Actual: 4, Expected: 0

These are the checks for buffer size. I could fix the numbers and the test would run. But if I were a good person, or playing one on TV, I’d at least wonder what was going on that made it change.

Basically, we’re seeing that the buffer never changes size. Ah. Right. It will start empty, and grow up to 4, then it stays at 4 forever. Our new logic that provides blank lines keeps the buffer full, forever. We can just delete those buffer check lines. Better, even, let’s correct them all to 4. I added a comment to the effect that once it gets to 4 it never goes back.

Tests run. Commit: Floater tests fixed, all tests green.

Now let’s do a bit on Combat. I still need some thinking, based on what we’re seeing now.

Combat

In the present scheme, an Encounter coroutine is created whenever a monster tries to move into the player’s square, or vice versa. We just use that coroutine now to produce the array of commentary and actions that represent the encounter, and then we play them (slowly) through the Floater crawl.

At the end of the crawl, the player is freed to move, and if she does, the monsters get a turn. This means that any nearby monsters can try to enter, and will thereby create an Encounter, which will be duly unrolled and stuffed into the crawl. Meanwhile, the princess has probably attacked. The upshot is that we might actually run two, or even three or four encounters sequentially. The princess might die long before these have run out, and other strange timing things can happen.

This used to be prevented by the crawl itself, which would not accept additional coroutines while it was running. So these attempts were made in the old scheme, but the only indication was a message on the console to the effect that an attempt to start an additional crawl was ignored.

So let’s start our thinking in the middle of a situation. Imagine the princess and three monsters arranged this way:

-M-
M-M
-P-

Now the princess, feeling brave, decides to move upward:

-M-
MPM
---

Now it is the monsters’ turn, and every one of them will try to move to her square and attack her. That will happen in a single turn, since they all move, one after another, in the “monsters’ turn”.

My current plan for combat is that it will encompass just one cycle of a Dungeons & Dragons style combat round. Initiative is rolled. Monsters or Princess go first. When it is an entity’s turn, they can move or attack. Roll d20 > defender’s Armor Class for a hit. Roll damage for the weapon in hand. Subtract from defender. Defender’s turn, ditto. Round over.

Recall that we’re working to synchronize some effects with the crawl, not with the program’s internal state. We’d prefer that the monsters’ and player’s attribute sheets show hit points declining as the crawl announces a hit, and that we’d like them to flash or scream at that time. We’re doing that by putting invisible commands in the crawl, which evoke the various effects at the right time.

This includes applying damage only at the time the crawl displays it. So this sequence is possible:

  1. Monster A hits princess, who has 10 points.
  2. Monster A rolls 15 points damage
  3. Buffer: Subtract 15 from princess
  4. Buffer: “Monster A hits for 15!”
  5. Monster B hits princess.
  6. Monster B rolls 10 damage. 7 Buffer: Substract 10 from princess
  7. Buffer: “Monster B hits for 10!” 9 Crawl finally subtracts 15 from Princess.
  8. Princess is dead
  9. Crawl subtracts 10 more from Princess
  10. Crawl says “Monster B hits for 10!”
  11. Princess is even more dead.

I think it’ll be acceptable if we can ensure that we only “discover” that the defender is dead at the end of the cycle. We can do that with a hidden call to check the death and add in the message at that time, right at the end of the combat cycle.

Let’s try to make it work as above, roughly like this:

First attempt to move into another entity’s tile creates a CombatRound with moving entity as attacker, and the other as defender. Subsequent attempts in the same turn cause the moving entity to be added to the CombatRound, as attacker or defender depending on whether the monster initiated the attack or the player did. All monsters get added in if they try to move on her.

At the end of the monster move cycle, if there’s a CombatRound, we run it. It plays out the round and produces the crawl.

This sounds good to me. Let’s try some TDD, which should help me shape this thing.

CombatRound TDD

function testCombatRound()
    CodeaUnit.detailed = true
    
    _:describe("CombatRound", function()
        
        _:before(function()
        end)
        
        _:after(function()
        end)
        
        _:test("Create Player-initiated Round", function()
        end)
    
    end)
    
end

So far so good. I’m imagining two factory methods, one for Rounds initiated by player and one for monsters. We may find that we don’t need these, but for now at least it’ll help me keep my head on straight.

The setup might be something like this:

        _:test("Create Player-initiated Round", function()
            local fakePlayer = FakePlayer()
            local m1 = FakeMonster()
            local round = CombatRound:forPlayer(fakePlayer, m1)
            local m2 = FakeMonster()
            round:addCombatant(m2)
        end)

I’m starting with Fake objects here, for two reasons. First, both Monster and Player are too wired into the system, with connections to GameRunner and Tiles and whatnot. That makes them difficult to use in tests of other object such as this one. Second, I want to drive out the combat-specific behavior for the combatants.

So I’ll create the fake objects and then the CombatRound. Right away I am reminded that I already have a FakePlayer class, used in the Loot tests. I could combine them, but I really want them to be separate. I’d keep them entirely local to the different tests if I could. For now, I’ll rename.

        _:test("Create Player-initiated Round", function()
            local fakePlayer = FakeCombatPlayer()
            local m1 = FakeCombatMonster()
            local round = CombatRound:forPlayer(fakePlayer, m1)
            local m2 = FakeCombatMonster()
            round:addCombatant(m2)
        end)

In the course of creating the shell classes. I realize that I have no need for the factory yet, so I rewrite the test to be simpler. With the shells, I now have:

        _:test("Create Player-initiated Round", function()
            local fakePlayer = FakeCombatPlayer()
            local m1 = FakeCombatMonster()
            local round = CombatRound(fakePlayer, m1)
            local m2 = FakeCombatMonster()
            round:addCombatant(m2)
        end)

CombatRound = class()

function CombatRound:init(player, monster)
end

function CombatRound:addCombatant(monster)
end

FakeCombatPlayer = class()

FakeCombatMonster = class()

Test runs fine. No expectations, no failures. Maybe we should assert something, drive out some behavior.

I’m not dealing with surprise, but I do want to deal with initiative. Even if the player initiates the combat, it is possible that the monsters get first hit, and vice versa.

We have a sticky issue to deal with. In use, this object will be driven by random rolls throughout. We do have the ability to plug in a fake random number generator in some other test, and we may well wind up using it here. But I’d like to defer that as long as possible, because it’s a pain to craft values to drive out the exchange we want. Let’s see what we can do.

Imagine that somewhere inside the CombatRound, it gets told to do its thing. First it rolls initiative against the player and monsters. This is a “dexterity check”. Let’s imagine that that’s done and then the function that does that simply makes a call to initialize some kind of state variable that says whether it is the monsters’ turn or the player’s. We’ll just call that directly:

            round:initiativeIsPlayers()

Then the round will start in earnest, probably with some sequence like this:

    self:initiativeIsPlayers()
    self:firstTurn()
    self:secondTurn()
    self:checkDeath()

I’m just sketching here. I really don’t know what this object should be like in detail. I’m in discovery. In creation. In imagination.

The turns will be basically the same. Imagine that both attacker and defender members in CombatRound are collections of one or more players or monsters. So we can loop over either one without regard to whether we’re dealing with a monster or a player.

This idea may not bear weight. The player wants the ability to choose who to attack: the monsters have no choice. We’ll see what happens when we get there.

The CombatRound, as I envision it, produces an array of strings and commands for the crawl. I guess we should assert against that and then make it work. We’ll surely need some kind of random number fake but we’ll deal with that when it happens.

        _:test("Create Player-initiated Round", function()
            local fakePlayer = FakeCombatPlayer()
            local m1 = FakeCombatMonster("Serpent")
            local round = CombatRound(fakePlayer, m1)
            local m2 = FakeCombatMonster("Spider")
            round:addCombatant(m2)
            round:initiativeIsPlayers()
            local r = round:getResults()
            _:expect(r[1]).is("Princess attacks Serpent!")
            _:expect(r[2]).isnt(nil) -- damage
            _:expect(r[3]).is("Princess does 5 damage!")
            _:expect(r[4]).is("Serpent attacks Princess!")
            _:expect(r[5]).isnt(nil) -- damage
            _:expect(r[6]).is("Serpent does 6 damage!")
            _:expect(r[7]).is("Spider attacks princess")
            _:expect(r[8]).isnt(nil) -- damage
            _:expect(r[9]).is("Spider does 3 damage!")
            _:expect(r[10]).isnt(nil) -- check death
        end)

So it might go something like this. I’m almost wishing I had chosen a simpler test. If need be, I can trim this one, but let’s see what happens. Looks like getResults is the hot item.

function CombatRound:init(player, monster)
end

function CombatRound:addCombatant(monster)
end

function CombatRound:initiativeIsPlayers()
end

function CombatRound:getResults()
    self.results = {}
    return self.results
end

This fails to return the first message. (And all the others.)

1: Create Player-initiated Round  -- Actual: nil, Expected: Princess attacks Serpent!

I’m going to save the player as is, and monsters in a collection, despite earlier ideas. I typed in a lot of mostly boilerplate:

CombatRound = class()

function CombatRound:init(player, monster)
    self.player = player
    self.monsters = { monster }
end

function CombatRound:addCombatant(monster)
    table.insert(self.monsters, monster)
end

function CombatRound:initiativeIsPlayers()
end

function CombatRound:getResults()
    self.results = {}
    self:addResult(self.player:name().." attacks "..self.monsters[1]:name())
    return self.results
end

FakeCombatPlayer = class()

function FakeCombatPlayer:init(name)
    self.cognomen = name
end

function FakeCombatPlayer:name()
    return self.cognomen
end

FakeCombatMonster = class()

function FakeCombatMonster:init(name)
    self.cognomen = name
end

function FakeCombatMonster:name()
    return self.cognomen
end

Message is:

1: Create Player-initiated Round -- TestCombatRound:56: attempt to concatenate a nil value

That’s the big line here:

function CombatRound:getResults()
    self.results = {}
    self:addResult(self.player:name().." attacks "..self.monsters[1]:name())
    return self.results
end

I’m not sure what’s up here. Let’s do some more basic checks before getting down to business:

I finally realize that I failed to provide the player name in the init.

            local fakePlayer = FakeCombatPlayer("Princess")

Now I get the error I expect:

1: Create Player-initiated Round -- TestCombatRound:56: attempt to call a nil value (method 'addResult')
function CombatRound:addResult(aResult)
    table.insert(self.results, aResult)
end
1: Create Player-initiated Round  -- Actual: Princess attacks Serpent, Expected: Princess attacks Serpent!

Forgot the bang.

    self:addResult(self.player:name().." attacks "..self.monsters[1]:name().."!")

The message is now:

1: Create Player-initiated Round  -- Actual: nil, Expected: nil

This confusing output is an anomaly in the isnt check of CodeaUnit, which doesn’t say something like “Expected anything except: nil”. On another day we’ll see about improving this product.

I’m starting to wish I had written a simpler test. I’ll revise history by removing all that stuff from the test, at least by commenting it out. And I’m thinking I’m going to want a “next” real soon now.

Isn’t there some way that Codea can do that? Let me do a bit of research. Yes. This test runs:

        _:test("Next", function()
            local ary = {"a", "b", "c"}
            local i,v
            i,v = next(ary,i)
            _:expect(v).is("a")
            i,v = next(ary,i)
            _:expect(v).is("b")
            i,v = next(ary,i)
            _:expect(v).is("c")
        end)

Maybe that will make our job easier. Let’s remove all the assertions but the one that runs and then see what to do:

        _:test("Create Player-initiated Round", function()
            local fakePlayer = FakeCombatPlayer("Princess")
            local m1 = FakeCombatMonster("Serpent")
            local round = CombatRound(fakePlayer, m1)
            local m2 = FakeCombatMonster("Spider")
            round:addCombatant(m2)
            round:initiativeIsPlayers()
            local r = round:getResults()
            _:expect(r[1]).is("Princess attacks Serpent!")
        end)

That passes, of course. Now, when I was walking “down the hall” just now, I had an idea. Could a CombatRound calculate all its roll results in one go, and then just enumerate them for its output? If it could, then we could create concrete tests by injecting the roll results table.

Let’s see … the rolls would be:

  1. Initiative, player or monster. Assume player.
  2. Attack success, true/false
  3. Attack hit points if true
  4. for each monster: a. attack success, true/false b. attack hit points if true
  5. check all for death

Hm. If this works, we can separate the calculation of the round from the reporting, almost entirely. The round turns into a short series of–let’s call them CombatEvents. Each such event creates some number of lines in the result array.

I’m going to try that, recasting the CombatRound such that we send it messages about the events and it records them.

        _:test("Create Player-initiated Round", function()
            local i,v
            local pl = FakeCombatPlayer("Princess")
            local m1 = FakeCombatMonster("Serpent")
            local round = CombatRound(fakePlayer, m1)
            local m2 = FakeCombatMonster("Spider")
            round:addCombatant(m2)
            round:attack(pl,m1)
            local r = round:getResults()
            i,v = next(r,i)
            _:expect(v).is("Princess attacks Serpent!")
        end)
function CombatRound:init(player, monster)
    self.player = player
    self.monsters = { monster }
    self.results = {}
end

function CombatRound:attack(entity1, entity2)
    self:addResult(entity1:name().." attacks "..entity2:name())
end

function CombatRound:getResults()
    return self.results
end

I rather expect this to work. It does, except that I forgot the bang again.

OK, now the hit:

            round:attack(pl,m1)
            round:damage(p1,m1,5)
            local r = round:getResults()
            i,v = next(r,i)
            _:expect(v).is("Princess attacks Serpent!")
            i,v = next(r,i) -- skip command
            i,v = next(r,i)
            _:expect(v).is("Serpent takes 5 damage!")

We skip the command line for now.

function CombatRound:damage(e1,e2,damage)
    self:addResult(e2, "damageFrom", nil, damage)
    self:addResult(e2:name().." takes "..damage.." damage!")
end

But we do produce the command line. I expect this to work:

And it does.

Does This Strike You as Odd?

One moment, we were creating an object that had all kinds of random behavior, and we were trying hard to avoid having to deal with fake random numbers. Moments later, we have an object in hand that is sent sensible messages like

    round:damage(entity1, entity2,damage)

And it just formats output lines in the results table. We haven’t dealt with the random bits at all, and it seems that we are somehow deferring the important part. Or are we?

My suspicion is that, yes, we’re certainly deferring the random bit, but that it’s not “the important bit”. Instead, we are just separating two concerns, the creation of the information we need to produce the output, and the production thereof. The combat logic will have to be implemented, and it will have to roll random numbers all over.

So this TDD exercise has done what it’s supposed to do, it has begun to drive out a design. Shaping this object and working with it has created some ideas in my mind (and yours, I hope) that we can use to create the object or objects that we want.

Now is is 1245. I started late this morning, probably around 0945, but we’re still three hours in, and we have zero lines of production code written. Is this a concern? Not to me: my job is to work out how to do the things my customer (my other head) wants, and to do them. Sometimes that’s just typing in things that I understand. Sometimes it’s working to build understanding.

At this point, I am ready to try a simple CombatRound object, along the lines we have here. I’m going to put it in place, not TDD it. TDD has done enough for me–I think.

Am I right? We’ll see, won’t we?

I’ll commit the current test and fake objects: CombatRound experimental test tab.

To make this make sense, I think I need to remove the ability to create the Encounter thing entirely. So I’m not sure whether this is a spike to learn, or whether it’s our first implementation of new combat. New Combat is only one round of combat, while the encounter is a whole little playlet. At some point, that reduction in game behavior has to turn up. Maybe today’s the day.

We’ll disable the monster behavior startActionWithPlayer, and change player’s startActionWithMonster to create a CombatRound.

function Monster:startActionWithPlayer(aPlayer)
    --[[
    if aPlayer:isDead() then return end
    self.runner:runBlockingCrawl(createEncounter(self,aPlayer))
    --]]
end

function Player:startActionWithMonster(aMonster)
    if aMonster:isDead() then return end
    CombatRound(self, aMonster)
end

I think I’ll just assume that it starts and does its thing, adding to the crawl like that guy in the nightly news that stands outside the building where the action took place hours before.

-- CombatRound
-- RJ 202102028

CombatRound = class()

function CombatRound:init(player,monster)
    self.player = player
    self.monsters = { monster }
    self.results = {}
    self:run()
    self:report()
end

function CombatRound:addResult(item)
    table.insert(self.results,item)
end

function CombatRound:report()
    self.player.runner:runCrawl(self.results)
end

function CombatRound:run()
    self:addResult(self.player:name().." attacks "..self.monsters[1]:name().."!")
end

With this much in place, when the princess tries to step on a Serpent, the message “Princess attacks Serpent!” comes out, just as we had in mind.

Let’s do a simplified thing here, and damage the creature randomly, and then, as a next step, check for dead.

This is interesting. Here’s the object so far:

function CombatRound:init(player,monster)
    self.player = player
    self.runner = player.runner -- sorry
    self.monsters = { monster }
    self.results = {}
    self:run()
    self:report()
end

function CombatRound:addResult(item)
    table.insert(self.results,item)
end

function CombatRound:monsterName(i)
    return self.monsters[i]:name()
end

function CombatRound:playerName()
    return self.player:name()
end

function CombatRound:report()
    self.player.runner:runCrawl(self.results)
end

function CombatRound:run()
    self:addResult(self.player:name().." attacks "..self.monsters[1]:name().."!")
    local damage = math.random(1,self.player:strength())
    self:addResult({ self.monsters[1], "damageFrom", nil, damage} )
    self:addResult(self:monsterName(1).." takes "..damage.." damage!")
end

And here’s what happens:

combat.mov

As expected, monsters can no longer attack. (We could let them run the encounter, but no.) When the princess attacks, she always hits, and she does damage. When the round is over … someone is displaying “Monster is dead!” for us!. That must be happening in the damageFrom method? Yes:

function Entity:damageFrom(aTile,amount)
    if not self:isAlive() then return end
    self.healthPoints = self.healthPoints - amount
    if self.healthPoints <= 0 then
        self.runner:addToCrawl({self:name().." is dead!"})
        sound(self.deathSound, 1, self.pitch)
        self:die()
    else
        sound(self.hurtSound, 1, self.pitch)
    end
end

Sweet. This is just about good enough to ship. In fact, I think I’ll stop here and sum up. You can ship this or ship yesterday’s, it’s up to you.

Summing Up

Today’s session was interesting. It wasn’t pure TDD: we didn’t write a series of tests, making each one run, until we wound up with the solution we wanted. Instead, we wrote some tests against fake objects, taking one pass after another at sussing out the shape of the objects we wanted.

Finally it came to me to split apart the random bits from the output bits. It seemed–and still seems–that we could compute all the results and then interpret them into the output. But that’s not what I did, is it? With the idea of putting stuff into the output in mind, I’ve started down the more conventional path of rolling a random value and applying it, then rolling the next.

I’m not sure if we’ll wind up with a two-stage object or not, one that computes a rack of numbers and then outputs the result. But the idea of that was what gave me enough of a clue to start writing the object.

The CombatRound object is presently so simple that you’d think I could have just typed it in. The thing is, however, that I couldn’t type it in because I didn’t have it in my head. I can only type whatever’s in my head. So the TDD exercise had value in getting it into my head.

Shall I keep that tab? I’ll keep it for now, but I am inclined to toss it, because it doesn’t really reflect my current thinking. It shaped my thinking, but doesn’t represent it.

As for the new object: it would perhaps be nice to have some tests for it. I’m not sure how to do that in a productive way. Perhaps smarter friends will offer suggestions.

See you next time!


D2.zip