I’m sorry–I have to do some planning. Then maybe some code.

The issue is combat. I think I could probably incrementally improve it and get it somewhere good, and I certainly intend to build it that way, but I want to muse a bit about how I want it to work, so that I can start out in the right direction.

Right now, combat is brief. When a monster or player tries to enter a tile where a possible combatant resides, the entering party “has initiative”, and starts action against the other. The pattern is symmetric on both sides:

function Player:startActionWithMonster(aMonster)
    aMonster:damageFrom(self.tile,math.random(0,self:strength()))
end

function Monster:startActionWithPlayer(aPlayer)
    aPlayer:damageFrom(self.tile,math.random(0,self:strength()))
end

The attacking party just deals damage from zero up to their current strength to the attacked party, and they handle it this way:

function Monster:damageFrom(aTile,amount)
    if not self.alive then return end
    self.healthPoints = self.healthPoints - amount
    if self.healthPoints <= 0 then
        self.healthPoints = 0
        sound(asset.downloaded.A_Hero_s_Quest.Monster_Die_1)
        self:die()
    else
        sound(asset.downloaded.A_Hero_s_Quest.Monster_Hit_1)
    end
end

function Player:damageFrom(aTile,amount)
    if not self:isAlive() then return end
    sound(asset.downloaded.A_Hero_s_Quest.Hurt_1, 1, 1.5)
    self.healthPoints = self.healthPoints - amount
    if self.healthPoints <= 0 then
        self.healthPoints = 0
        sound(asset.downloaded.A_Hero_s_Quest.Hurt_3,1,1.5)
        self:die()
    end
end

There’s rather a lot of duplication here. And by and large, I think combat will always be symmetrical in this sense, even as it gets more elaborate. This duplication will become more of a concern, and we’ll probably want to get rid of it. If we don’t, we’ll surely make a mistake in one or another of the copies.

For now, I’m just musing on how it “should” work, to get my thoughts a bit more aligned.

Musing

In no particular order other than the order they come to mind …

Permadeath
Currently when the player dies, the game is effectively over. While permadeath is not uncommon in roguelike games, I’m not sure that’s what I want. We’ll just keep that in mind.
Fairness
At every level–when we have levels–the player should have a fair chance of survival. Perhaps the monsters are a bit more powerful than the player, to make it interesting, but with care, the player should be able to prevail.
Rolling Attribute against Attribute
I have in mind a general technique of comparing two rolls on a given attribute to make a decision. Both player and monster produce a value from 0 through X, their current attribute value, and the larger of those values is “better”. (Or possibly worse, point is we compare them.) This will allow a stronger opponent to sometimes be parried by a weaker one, or a slower opponent to sometimes outpace a faster one.
Surprise and Initiative
Currently, the game seems to assume that the player only enters a tile with intent to attack, and that the monster therein is unaware of her presence, so the player gets first strike. Imagine an attribute, perhaps Speed, such that we roll against Speed, and if the monster wins, it parries the blow, reducing damage, or taking no damage at all.
Riposte
Especially if the opponent has blocked the incoming blow, they should have a chance at a riposte, a responsive attack in the same turn. This would probably be a low-probability roll against their own speed. I think a riposte probably carries relatively low damage, and perhaps it cannot be blocked.
Possible Attributes
We have Strength and Health now. They seem to overlap in my thinking. I believe I’ll add Speed, for sure. There is a Courage value presently shown, just because I used it to test out the bar graph, but we could imagine something like that affecting how well the player performs.
I mentioned last time that D&D has attributes of Strength, Dexterity, Constitution, Intelligence, Wisdom, and Charisma. I was thinking that perhaps our equivalents to strength, dexterity, and constitution, namely strength, speed, and health, might suffice for our purposes. That still seems about right.
Aggressive?
Some monsters might be aggressive, actively trying to get at the player, while others might not be, unless attacked. That property would not be displayed, although you could infer it from the monster’s behavior. We could mask it a bit by making even aggressive monsters move away from the player once in a while, making it harder to detect the aggressive ones. Probably within some short range, aggressive ones would charge right in.
Speed
Monsters presently move freely, based on a somewhat random timer, about once every second to 1.5 seconds. We could make different monsters move more or less frequently based on their speed. This would also increase the likelihood of an attack from an aggressive monster, since it cycles more often.
Reporting
There will be a number of “events” during a battle. A hit, a block, a riposte, damage done, and so on. I was checking out the ancient game Rogue briefly last night, and it uses a scrolling text window to report what’s going on. The Codea Simple Dungeon game uses a “FloatingMessage”, which is a text message that appears over a creature and floats upward, fading as it goes. The latter might be easier, so I’ll probably start with that.

Sketching an Encounter

Let’s imagine how an encounter might go. Alice enters a tile containing Bob.

  1. Compare Speed rolls. Perhaps entering party gets a small edge. Suppose Bob wins the comparo.
  2. Bob will initiate attack. Rolls a damage number based on his Strength, sends damage to Alice.
  3. Alice again compares Speed rolls with Bob. If she wins, she blocks the attack, wholly (or optionally partially). She takes zero or more damage.
  4. If Alice blocked, she can try a difficult roll against her Speed for a riposte. If she succeeds, she rolls damage based on strength, reduced because it’s a riposte, and sends the damage to Bob. There is no blocking a riposte. No one’s that good. (Suggests an indicator on the damage message, or two damage messages, one unblockable.)
  5. The encounter is over.

Simpler encounters are of course possible:

  1. Compare Speed Rolls. Alice wins the comparo.
  2. Alice will initiate. Rolls a damage number, sends damage to Bob.
  3. Bob rolls speed, fails the roll, takes the damage.
  4. Encounter over.

I believe the process is entirely symmetric. This tempts me to create a new object, perhaps Encounter, that contains two entities, Attacker and Resident, that manages and reports the encounter. It occurs to me as I write this that the Encounter object could probably be fairly easily test-driven, and since it definitely contains “branching logic”, it’s the sort of object that will definitely benefit from TDD. Let’s see what we can do.

TDDing Encounter

Let’s imagine that when Mover enters a tile containing Resident, their startEncounterWith will create an Encounter listing them as Attacker and Resident as resident.

Despite the admonition “Tell, Don’t Ask”, I suspect that Encounter will ask questions and make the comparisons. After we get that in place, maybe we’ll see a better way. For now we can just create Encounters and test behavior into them. I’ll start a new test tab:

-- RJ 20210104
-- 

function testEncounter()
    CodeaUnit.detailed = true
    
    _:describe("Encounters", function()
        
        _:before(function()
        end)
        
        _:after(function()
        end)
        
    end)
end

Now to test something:

        _:test("Encounter compares on speed", function()
            
        end)

Ha. Already a problem. We don’t have speed attribute. Well, I’m sure our test will drive those out as well. And what about random numbers, which we’ll be using all the time? Those are always a pain to TDD, and we may have to use some kind of test double or other trick to support them.

For now, the big fool will just press on …

In prior tests I’ve had to set up a GameRunner and Tiles. Let’s see if we can do without. This might get tricky, but if we can get it going, it will make future tests easier as well.

        _:test("Encounter compares on speed", function()
            local monster = Monster(nil,nil,Monster:getMtEntry(1))
            local player = Player(nil,nil)
        end)

This is probably sufficient to explode. Let’s find out.

1: Encounter compares on speed -- Monster:12: attempt to index a nil value (field 'tile')
function Monster:init(tile, runner, mtEntry)
    if not MT then self:initMonsterTable() end
    self.alive = true
    self.tile = tile
    self.tile:addContents(self)

Let’s just create a tile if we don’t have one …

    self.tile = tile or Tile:room(100,100,runner)
1: Encounter compares on speed -- Player:21: attempt to index a nil value (field 'tile')

We’ll try the same thing in Player:

function Player:init(tile, runner)
    self.alive = true
    self.tile = tile or Tile:room(101,100, runner)
1: Encounter compares on speed -- Tile:195: attempt to index a nil value (field 'runner')

I’m just following standard TDD procedure here, fixing whatever doesn’t work, but of course I’m also making some dangerous changes if anyone ever accidentally creates players and monsters without all the hookups.

function Tile:illuminateLine(dx,dy)
    local pts = Bresenham:drawLine(0,0,dx,dy)
    for i,offset in ipairs(pts) do
        local pos = self:pos() + offset
        local d = self:pos():dist(pos)
        if d > 7 then break end
        local t = math.max(255-d*255/7,0)
        if t > 0 then self.seen = true end
        local tile = self.runner:getTile(pos)
        tile:setTint(color(t,t,t))
        if tile.kind == TileWall then break end
    end
end

Here it gets interesting. This is fairly deep down in the guts of things. Makes me think we would do better to set up properly. Let’s back out those two changes and set up a “before”:

    _:describe("Encounters", function()
        
        local runner
        local room
        
        _:before(function()
            runner = GameRunner()
            room = Room(1,1,20,20, runner)
        end)
        
        _:after(function()
        end)
        
        _:test("Encounter compares on speed", function()
            local mtile = Tile:room(10,10, runner)
            local monster = Monster(mtile,runner,Monster:getMtEntry(1))
            local ptile = Tile:room(11,10, runner)
            local player = Player(ptile,runner)
        end)
        
    end)

This passes. Of course it doesn’t do anything yet.

What we want to do now is to pretend that player has moved onto monster, and change that to create and use an Encounter. This is perilously close to difficult. At least just now I don’t see how I’m going to do it. Plow on. This much is obvious:

        _:test("Encounter compares on speed", function()
            local mtile = Tile:room(10,10, runner)
            local monster = Monster(mtile,runner,Monster:getMtEntry(1))
            local ptile = Tile:room(11,10, runner)
            local player = Player(ptile,runner)
            player:startActionWithMonster(monster)
        end)

How can we get our hands on the Encounter that’s about to be created? Also, do we really want to use startActionWith? No … if we do, we can’t release again until this works. Instead, a new method:

        _:test("Encounter compares on speed", function()
            local mtile = Tile:room(10,10, runner)
            local monster = Monster(mtile,runner,Monster:getMtEntry(1))
            local ptile = Tile:room(11,10, runner)
            local player = Player(ptile,runner)
            player:startEncounterWithMonster(monster)
        end)

No! I see it. We’ll create the encounter in TileArbiter. Thus we can create one here.

        _:test("Encounter compares on speed", function()
            local mtile = Tile:room(10,10, runner)
            local monster = Monster(mtile,runner,Monster:getMtEntry(1))
            local ptile = Tile:room(11,10, runner)
            local player = Player(ptile,runner)
            local encounter = Encounter(player, monster)
            _:expect(encounter.attacker).is(player)
            _:expect(encounter.defender).is(monster)
        end)

Oh, I’m liking the feeling here. Now to fix the expected failures:

1: Encounter compares on speed -- TestEncounter:25: attempt to call a nil value (global 'Encounter')
-- Encounter
-- RJ 20210104

Encounter = class()

function Encounter:init(attacker, defender)
    self.attacker = attacker
    self.defender = defender
end

Test runs. Now, however, we have to make it do things. That’s going to be interesting. Let’s suppose that we run an encounter by sending it the message attack. And I’ll sketch in what an elementary attempt to attack might do, and then we’ll see how to test that.

1: Encounter compares on speed -- TestEncounter:28: attempt to call a nil value (method 'attack')

Well, that might work like this:

function Encounter:attack()
    local damage = self:rollDamage(self.attacker:strength())
    self.defender:damageFrom(self.attacker)
end

I made rollDamage a method, because I plan to pass in some kind of fake object to let me control what happens here. As for the defender, we’ll have to see what we do there. I suppose we could just inspect him. I hope to defer creating any complicated test doubles.

Let’s extend Encounter this way:

function Encounter:init(attacker, defender, random)
    self.attacker = attacker
    self.defender = defender
    self.random = random or math.random
end

And use it this way:

function Encounter:rollDamage(aNumber)
    return self.random(0,aNumber)
end

And in our test, provide a magical random number generator:

local randomNumbers = {1}
local randomIndex = 0
function random(aNumber)
    randomIndex = randomIndex + 1
    return randomNumbers[randomIndex]
end

And use it in the test:

            local encounter = Encounter(player, monster, random)

And check the monster in and out:

        _:test("Encounter compares on strength", function()
            local mtile = Tile:room(10,10, runner)
            local monster = Monster(mtile,runner,Monster:getMtEntry(1))
            local ptile = Tile:room(11,10, runner)
            local player = Player(ptile,runner)
            local encounter = Encounter(player, monster, random)
            _:expect(encounter.attacker).is(player)
            _:expect(encounter.defender).is(monster)
            _:expect(monster:strength()).is(1)
            encounter:attack()
            _:expect(monster:strength()).is(0)
        end)

I renamed the test, since it’s just testing on strength. But will it work? I was hoping so.

1: Encounter compares on strength -- Monster:113: attempt to perform arithmetic on a nil value (local 'amount')
function Monster:damageFrom(aTile,amount)
    if not self.alive then return end
    self.healthPoints = self.healthPoints - amount
    if self.healthPoints <= 0 then
        self.healthPoints = 0
        sound(asset.downloaded.A_Hero_s_Quest.Monster_Die_1)
        self:die()
    else
        sound(asset.downloaded.A_Hero_s_Quest.Monster_Hit_1)
    end
end

I called that method seriously incorrectly. :)

function Encounter:attack()
    local damage = self:rollDamage(self.attacker:strength())
    self.defender:damageFrom(self.attacker.tile, damage)
end
1: Encounter compares on strength  -- Actual: 1, Expected: 0

One in, one out. This doth confuseth me. Imma print the damage before sending.

damage 	1

Hm. Could my monster possibly have strength 2? Um shouldn’t I be testing health? Yes, I should.

        _:test("Encounter compares on strength", function()
            local mtile = Tile:room(10,10, runner)
            local monster = Monster(mtile,runner,Monster:getMtEntry(1))
            local ptile = Tile:room(11,10, runner)
            local player = Player(ptile,runner)
            local encounter = Encounter(player, monster, random)
            _:expect(encounter.attacker).is(player)
            _:expect(encounter.defender).is(monster)
            _:expect(monster:health()).is(1)
            encounter:attack()
            _:expect(monster:health()).is(0)
        end)

The test is not yet complete, and as implemented, the Encounter is sure to make this test break very soon. But the good news is that I have a test in place.

Now what do we really want to do? And will it be sufficient to check results on the attacker and defender? Let’s assume so, and see where we get in trouble.

Let’s sketch in a bit more flow in attack:

function Encounter:attack()
    local attackerSpeed = self:rollRandom(self.attacker:speed())
    local defenderSpeed = self:rollRandom(self.defender:speed())
    if attackerSpeed > defenderSpeed then
        local damage = self:rollRandom(self.attacker:strength())
        self.defender:damageFrom(self.attacker.tile, damage)
    end
end

function Encounter:rollRandom(aNumber)
    return self.random(0,aNumber)
end

I renamed rollDamage to rollRandom as it is not concerned only with damage. Now we're going to get errors on calling speed()` on Monster:

1: Encounter compares on strength -- Encounter:13: attempt to call a nil value (method 'speed')

I love it when a plan comes together. Now we’ll stub out speed in Monster:

function Monster:speed()
    return 5
end

In passing, I wonder why monsters have that strengthPossible and strengthMax stuff. There’s no actual value to those. Make a yellow sticky note.

Now we’re going to ask for three random numbers, the two speeds and the damage. We want the player to win, so:

        _:test("Encounter compares on strength", function()
            randomNumbers = {3, 2, 1}
            local mtile = Tile:room(10,10, runner)
            local monster = Monster(mtile,runner,Monster:getMtEntry(1))
            local ptile = Tile:room(11,10, runner)
            local player = Player(ptile,runner)
            local encounter = Encounter(player, monster, random)
            _:expect(encounter.attacker).is(player)
            _:expect(encounter.defender).is(monster)
            _:expect(monster:health()).is(1)
            encounter:attack()
            _:expect(monster:health()).is(0)
        end)

I moved the setting of the random numbers inside the test because we’ll be using different numbers for further tests. This should work, I think, but I have a concern. First make it work:

We need speed on player also. Who could have known that? Well, anyone.

function Player:speed()
    return 5
end

Value doesn’t matter yet, the test is rolling its own.

1: Encounter compares on strength -- Encounter:15: attempt to compare two nil values

How odd.

Did we run the random number generator off the end? I don’t know. We should surely initialize its index.

local randomNumbers = {}
local randomIndex = 0
function random(aNumber)
    randomIndex = randomIndex + 1
    local rand = randomNumbers[randomIndex]
    if rand == nil then
        assert(false, "off end of random numbers "..randomIndex)
    end
    return randomNumbers[randomIndex]
end
1: Encounter compares on strength -- TestEncounter:46: off end of random numbers 1

I suspect that my assignment to randomNumbers has gone to a global. I’ll move those variables up in the test.

    _:describe("Encounters", function()
        
        local runner
        local room
        local randomNumbers = {}
        local randomIndex = 0
        
        _:before(function()
            randomNumbers = {}
            randomIndex = 0
            runner = GameRunner()
            room = Room(1,1,20,20, runner)
        end)
        
        _:after(function()
        end)
        
        _:test("Encounter compares on strength", function()
            randomNumbers = {3, 2, 1}
            local mtile = Tile:room(10,10, runner)
            local monster = Monster(mtile,runner,Monster:getMtEntry(1))
            local ptile = Tile:room(11,10, runner)
            local player = Player(ptile,runner)
            local encounter = Encounter(player, monster, random)
            _:expect(encounter.attacker).is(player)
            _:expect(encounter.defender).is(monster)
            _:expect(monster:health()).is(1)
            encounter:attack()
            _:expect(monster:health()).is(0)
        end)

I am hopeful that this will improve things.

It only sort of does:

1: Encounter compares on strength -- TestEncounter:44: attempt to perform arithmetic on a nil value (global 'randomIndex')

I moved the table and index inside the describe, and the function is outside. Let’s move them outside and at the top.

local randomNumbers = {}
local randomIndex = 0

function testEncounter()
    CodeaUnit.detailed = true
    
    _:describe("Encounters", function()
...

OK, it was a scoping issue. Test runs correctly. Now to my concern:

I’m testing the final outcome of the Encounter, but it’s the same outcome as before, even though a decision has now been made on speed. Let’s create two tests that should have different outcomes.

        _:test("Encounter with player faster", function()
            randomNumbers = {3, 2, 1}
            local mtile = Tile:room(10,10, runner)
            local monster = Monster(mtile,runner,Monster:getMtEntry(1))
            local ptile = Tile:room(11,10, runner)
            local player = Player(ptile,runner)
            local encounter = Encounter(player, monster, random)
            _:expect(encounter.attacker).is(player)
            _:expect(encounter.defender).is(monster)
            _:expect(monster:health()).is(1)
            encounter:attack()
            _:expect(monster:health()).is(0)
        end)
        
        _:test("Encounter with monster faster", function()
            randomNumbers = {2, 3, 1}
            local mtile = Tile:room(10,10, runner)
            local monster = Monster(mtile,runner,Monster:getMtEntry(1))
            local ptile = Tile:room(11,10, runner)
            local player = Player(ptile,runner)
            local encounter = Encounter(player, monster, random)
            _:expect(encounter.attacker).is(player)
            _:expect(encounter.defender).is(monster)
            _:expect(monster:health()).is(1)
            encounter:attack()
            _:expect(monster:health()).is(1)
        end)

I renamed the tests again. Now the second test should find that the monster is faster. As the code is written now, the monster will not be damaged, nor will the player. But let’s run and see what does happen.

Both tests run correctly. This is consistent with the current implementation:

function Encounter:attack()
    local attackerSpeed = self:rollRandom(self.attacker:speed())
    local defenderSpeed = self:rollRandom(self.defender:speed())
    if attackerSpeed > defenderSpeed then
        local damage = self:rollRandom(self.attacker:strength())
        self.defender:damageFrom(self.attacker.tile, damage)
    end
end

I think the player should get the advantage if the values are equal. Let’s add another test for that:

        _:test("Encounter with equal speed, player wins", function()
            randomNumbers = {3, 3, 1}
            local mtile = Tile:room(10,10, runner)
            local monster = Monster(mtile,runner,Monster:getMtEntry(1))
            local ptile = Tile:room(11,10, runner)
            local player = Player(ptile,runner)
            local encounter = Encounter(player, monster, random)
            _:expect(encounter.attacker).is(player)
            _:expect(encounter.defender).is(monster)
            _:expect(monster:health()).is(1)
            encounter:attack()
            _:expect(monster:health()).is(0)
        end)

I expect this to fail …

3: Encounter with equal speed, player wins  -- Actual: 1, Expected: 0

And I expect this to fix it:

    if attackerSpeed >= defenderSpeed then
        local damage = self:rollRandom(self.attacker:strength())
        self.defender:damageFrom(self.attacker.tile, damage)
    end

Mysteriously enough, it does. About time to commit, I think. All the tests are good and no one in the game is actually using the Encounter, so we can commit: Encounter object coming into being.

Now fact is, we could probably actually use this object right now. Since we’re right at a commit, and can easily revert, let’s see how hard it might be to use Encounter. We’ll have to look at TileArbiter, and decide whether it is modified to create the Encounter, or whether we are going to create it elsewhere.

Currently the arbitration table is this:

function TileArbiter:createTable()
    -- table is [resident][mover]
    if TA_table then return end
    local t = {}
    t[Chest] = {}
    t[Chest][Monster] = {moveTo=TileArbiter.refuseMove}
    t[Chest][Player] = {moveTo=TileArbiter.refuseMove, action=Player.startActionWithChest}
    t[Key] = {}
    t[Key][Monster] = {moveTo=TileArbiter.acceptMove}
    t[Key][Player] = {moveTo=TileArbiter.acceptMove, action=Player.startActionWithKey}
    t[Player] = {}
    t[Player][Monster] = {moveTo=TileArbiter.refuseMoveIfResidentAlive, action=Monster.startActionWithPlayer}
    t[Monster]={}
    t[Monster][Monster] = {moveTo=TileArbiter.refuseMoveIfResidentAlive, action=Monster.startActionWithMonster}
    t[Monster][Player] = {moveTo=TileArbiter.refuseMoveIfResidentAlive, action=Player.startActionWithMonster}
    t[Health] = {}
    t[Health][Player] = {moveTo=TileArbiter.refuseMove, action=Player.startActionWithHealth}
    TA_table = t
end

The assumption is that the action is always sent to the mover. So the easiest thing might be to change the startActionWith methods:

function Player:startActionWithMonster(aMonster)
    aMonster:damageFrom(self.tile,math.random(0,self:strength()))
end

That becomes …

function Player:startActionWithMonster(aMonster)
    Encounter(self,aMonster):attack()
end

I think this could work, so I’ll play a bit. It seems to work as before, and it certainly should, except that sometimes an attempted attack may do no damage. I’d like to change that because of the sounds that get made.

function Monster:damageFrom(aTile,amount)
    if not self.alive then return end
    self.healthPoints = self.healthPoints - amount
    if self.healthPoints <= 0 then
        self.healthPoints = 0
        sound(asset.downloaded.A_Hero_s_Quest.Monster_Die_1)
        self:die()
    else
        sound(asset.downloaded.A_Hero_s_Quest.Monster_Hit_1)
    end
end

Currently the monster barks even if the damage was zero. We could change that in the monster, and I guess we should. But let’s change Encounter not to deal the damage if it is zero.

function Encounter:attack()
    local attackerSpeed = self:rollRandom(self.attacker:speed())
    local defenderSpeed = self:rollRandom(self.defender:speed())
    if attackerSpeed >= defenderSpeed then
        local damage = self:rollRandom(self.attacker:strength())
        if damage > 0 then
            self.defender:damageFrom(self.attacker.tile, damage)
        end
    end
end

This will cause no sound to appear when you attack and roll no damage, because it’s not even sent to the defender.

That works as anticipated. I attempt to move onto a monster, nothing happens. Other times, it barks, and finally of course dies.

Let’s change Monster to do the same:

function Monster:startActionWithPlayer(aPlayer)
    Encounter(self,aPlayer):attack()
end

Seems to work fine, except that pink slimes are almost unable to hit me. That will hold for most monsters I imagine, but the weaker ones especially so.

Right. Murder Hornet does a much better job of killing me, since it has higher strength than the Pink Slime. I can see that when it comes to setting up the speeds for these creatures, we’ll need to work on balancing a bit. Of course some of these creatures shouldn’t even appear on level zero, in a spirit of fairness. And some should be seriously dangerous when we do encounter them deeper down.

For now, commit: Encounter used for all battles.

Summing Up

This went just about as nicely as I could have wanted. Perhaps I have redeemed myself a bit from prior chaos. Using TDD to implement Encounter went pretty nicely. There are still some issues that my colleagues in the London school of testing would be nattering about.

Detroit school, of which I am surely a founding member, rarely uses test doubles, while the London school uses them quite commonly, indeed almost always. Ther use of mock objects, in particular, enables them to discern events which we cannot see in our tests here.

The Encounter object currently only makes one operational call on its member objects, damageFrom, although it also asks for speed and strength values. In our tests, we cannot directly detect that damageFrom was called. Instead, we determine that it “must have been called”, because the health of the target object changes. A judicious use of test doubles would allow us to discern that the method was called and what its input value was.

That’s not very important yet: the resultant health is quite close to the cause, the call to damageFrom. But as Encounter gets more complex, with blocking and ripostes possible, it is likely that we’ll have more difficulty checking results to check the flow of Encounter.

It might even be that we’ll have to invent a way to set up a test double of some kind to tell us what’s going on behind the scenes. I don’t exactly want to do that, because I’ve rarely used test doubles, and I’ll have to learn or invent some tricks to do it well. So I’m hoping it won’t be necessary, and thinking that it might be.

But for now, our Encounter is working as intended, and we can see what we do next, next time. See you then!


D2.zip