Dungeon 113
Friday morning
I wound up spending a few hours making 18 tests run that had been commented out. I had turned them off when I made some change that seemed to require big changes in those tests, and I knew that the changes I made were not what those tests were checking. So, in my laziness, I turned them off.
I had promised in Thursday’s article that I’d write a test that looked to be a pain to do, so to get started, I made those tests run. There was no material there for an article, just tedium. It was so boring that my editor program, Scrivener, dozed off three times before I stopped writing.
I still owe you that test, and I may or may not do it today. Here’s today’s action:
0745 Saturday
I noticed something significant last night. Something I don’t recall ever really thinking about before. Of course, in 8 decades you forget a lot …
During last night’s Zoom Ensemble, I was making some point about the differences between my dungeon program and Bryan’s, relating to Beth’s idea of a few days ago, and I chanced to look at the tests for Combat. Instead of using a real Player and Monster, they use instances of a special test double class, FakeEntity.
As I’ve mentioned, as a Detroit-school TDDer, I don’t often use test doubles. I have used a few in this program, mostly because the objects in the dungeon program are connected together–perhaps more than they should be–and setting up real objects for a test can be a real pain. I do think that yesterday’s exercise helped me find a pattern that can make it easier, but it’s still too tricky.
Now this is an important indication. When code is hard to test, it’s almost certainly not as simple and clean as it could be. There is something not quite right about it. We may not see what is wrong, we certainly may not see how to fix it, but the smart money says there is something needing improvement.
There is, of course, a more immediate problem. When tests are hard to write, we don’t write them. When we don’t have tests, we don’t find problems until we run the program, and exercise the specific code that has a lurking defect. When we do realize there’s a problem, the fact that there are no tests that find it means that we have to resort to running the program over and over to see if it is fixed. In the case of the dungeon program, we might have to wander around until we find a Death Fly to do battle with. It’s not a good thing.
Now in the tests for combat, I wanted to test the CombatRound object. It requires a Player instance and a Monster instance to do its work. They are difficult to set up properly, so I wrote the test double, FakeEntity, and used that. Since I was testing the combat logic, not the logic of player or monster, all I had to do was provide a few fake methods in FakeEntity.
I looked at that last night, and here’s what I saw:
Fake Entity
FakeEntity = class()
function FakeEntity:init(name)
self.cognomen = name
self.alive = true
end
function FakeEntity:accumulateDamage()
end
function FakeEntity:attackerSpeedAdvantage()
return 0
end
function FakeEntity:attackVerb()
return "whacks"
end
function FakeEntity:die()
self.alive = false
end
function FakeEntity:isAlive()
return self.alive
end
function FakeEntity:isDead()
return not self:isAlive()
end
function FakeEntity:name()
return self.cognomen
end
function FakeEntity:speedRoll()
return 5
end
function FakeEntity:willBeAlive()
return not self:willBeDead()
end
function FakeEntity:willBeDead()
return self:isDead()
end
As you can see, the fake methods rarely do much of anything, maybe store some status so that the thing acts dead when it’s dead, and so on.
That’s the code. Here’s what I saw:
In that object are all the methods of Player and Monster that have to do with combat, and no others.
You don’t seem excited enough by that discovery. Of course that’s what’s there, I only put methods in when CombatRound called them. But the point! The point is, combat doesn’t need a Player and a Monster to talk to. It only needs some kind of object that can respond to those few messages, and that can retain as much state as is necessary to progress us through combat.
If entities had an object like that, then they wouldn’t need to implement most of those messages, perhaps none of them. Entities would get simpler, and there’d be this new, also simple object, dealing just with combat issues.
Furthermore, let me remind you what Beth said in hir suggestion a few days ago:
I might see both the Princess and Monster classes having a Health class that they delegate taking damage too, which would end up with most of the shared logic. That would make sense if, for example, there was an effect that caused someone to take more or less damage, or if different monsters took different damage from different attack types.
Similarly, they might each have an Attack property, which could include the behavior of picking between the attack phrases. That would make something like “having different weapons” easy to implement.
Beth foresaw what might happen if we broke out a “health” class to delegate to. By the time we’re done it may not be called “Health”, but it can behave much as Beth predicted, and can unload a chunk of capability from the Entity.
Now, I was leaning in that direction, based on hir suggestion, but last night’s look at the FakeEntity brought it home to me: there can be a simple object owned by the Entity that holds all the attribute information needed for combat, and perhaps, for other interactions within the dungeon.
But here’s the surprising lesson for me, if not for you:
This May Surprise You–It Did Me
The test double very clearly shows the protocol needed for combat.
Now if you’re a London School TDDer, you already know that. But I’ve only ever used test doubles “defensively”, to save me some work setting up a test of something else. I’ve not experienced using them to discover a protocol.
Now, of course I use TDD to discover a protocol all the time, but I do it to create the real object that I’m after. I TDD real things, not fake things. In this case, I was TDDing the CombatRound, or at least sort of TDDing it. That’s where I was focused. I didn’t notice that every time my FakeEntity didn’t know a method, or have the right fake response, I was also TDDing it.
I suspect that for the first time, I feel in my bones what London School folks know very well. I have no immediate plans to do anything about this, but I feel that I’ve definitely learned something.
I do plan, of course, to use the information in FakeEntity to work toward some new object that’ll be owned by the Entity and used in combat. Today, I don’t even know what to call it, but I know more about where it lives and what it looks like.
Whee! It’s always fun when you learn something.
What Shall We Do Today?
Well, we could just publish right now and move on with our lives. It is Saturday, after all. But let’s at least look at some things we need to do and see if there’s something we’d like to do now.
We’ve got our HealthAttribute working and in place, and it has mostly the same protocol as our FakeEntity, and a bit more:
HealthAttribute = class()
function HealthAttribute:init(value, max)
self._value = value
self._max = max or value
self._accumulatedDamage = 0
end
function HealthAttribute:accumulateDamage(damage)
self._accumulatedDamage = self._accumulatedDamage + damage
end
function HealthAttribute:addPoints(points)
self._value = math.max(0,math.min(self._value + points, 20))
end
function HealthAttribute:applyAccumulatedDamage(amount)
self._value = math.max(0, self._value - amount )
self._accumulatedDamage = math.max(0, self._accumulatedDamage - amount)
end
function HealthAttribute:die()
self._value = 0
self._accumulatedDamage = 0
end
function HealthAttribute:isAlive()
return not self:isDead()
end
function HealthAttribute:isDead()
return self._value == 0
end
function HealthAttribute:max()
return self._max
end
function HealthAttribute:value()
return self._value
end
function HealthAttribute:willBeAlive()
return not self:willBeDead()
end
function HealthAttribute:willBeDead()
return self._value - self._accumulatedDamage <= 0
end
Now we can’t just plug this thing in and have it take over combat, because we have issues of speed that play in there, and we’ll be using the other attributes as well, as time goes on.
Or can we? Suppose we change the CombatRound to work with two HealthAttributes instead of two Entities? We’d surely have things that we couldn’t get from the HA, but it could have a pointer back to the Entity, and could refer back until that info was taken over.
The name HealthAttribute would be wrong. Bill and Bryan mentioned the D&D term “Character Sheet” last night. Now that’s a massive concept, but maybe we’re working on an EntityAttributes class, not a HealthAttribute class.
As soon as we think of a new name for the thing, we begin to think about what’s in it. But we have some special behavior encapsulated in HealthAttribute right now, namely its ability to predict the future based on damage that has been accumulated but not yet applied. It’s really about Health. So maybe there’s a new object trying to be born.
And that sets my direction for me. Let’s see about enhancing the HealthAttribute to know how to draw itself, and at least begin to deal with the notion of a current maximum health, as we talked about last time.
I think we’ll call that special value the “nominal value”, the current maximum health you’re supposed to have. When you’re below nominal, you’ll slowly heal toward it, When you are above, you’re on some kind of uppers, and you’ll slowly decline toward nominal.
We create our HealthAttribute like this:
function Player:retainedAttributes()
return { keys=0, _health = HealthAttribute(12,12), speedPoints = 8, strengthPoints = 10 }
end
function HealthAttribute:init(value, max)
self._value = value
self._max = max or value
self._accumulatedDamage = 0
end
Let’s rename _max to _nominal. Easily done, max
method changed (used only in tests so far) to nominal
as well. Commit: rename max to nominal in HealthAttribute.
Now I want to see if we can defer the display logic down to HA.
function AttributeSheet:draw()
local m = self.monster
if not m:displaySheet() then return end
pushMatrix()
pushStyle()
resetMatrix()
zLevel(10-m:distanceFromPlayer())
rectMode(CORNER)
textMode(CORNER)
textAlign(LEFT)
self:drawParchment()
fill(0)
self:drawText(m:name())
self:newLine(2)
self:drawText("Health")
self:drawBarFraction(self.healthIcon, m:health(), 20)
self:newLine()
self:drawText("Speed")
self:drawBarFraction(self.speedIcon, m:speed(), 20)
self:newLine()
self:drawText("Strength")
self:drawBarFraction(self.strengthIcon, m:strength(), 20)
self:drawKeys()
self:drawPhoto(m:photo())
popStyle()
popMatrix()
end
Long but somewhat straightforward. The sheet is keeping track of the y coordinate to draw on, by translating downward:
function AttributeSheet:newLine(count)
for i = 1,count or 1 do
translate(0,-fontSize()-5)
end
end
That’s almost nice. I still like that idea. Now I’ve gotta ask myself: should the HealthAttribute know how to draw itself? Right now it knows how to keep track of some values. My habit has been to have objects know how to draw themselves, but one result of that is that they tend to have mixed purposes. What does our proposed HA do?
Well, it keeps track of the health of an entity, and oh yes, well, it also draws itself as a bar chart.
Not exactly single-purpose.
What is a line in our AttributeSheet? It is a drawing, a view, of some object (often just a value at present). For health, strength, and speed, it’s a bar chart.
Maybe we need a bar chart object. Let’s look at the bar chart method:
function AttributeSheet:drawBarFraction(icon, current, currentMax)
pushStyle()
tint(255)
spriteMode(CENTER)
rectMode(CORNER)
pushMatrix()
translate(80,10)
icon:draw()
popMatrix()
stroke(255)
fill(150,150,150)
rect(100,0,120,20)
--fill(255,0,0)
rect(100,0,120*currentMax/20,20)
fill(255,255,0)
rect(100,0,120*current/20,20)
popStyle()
end
Let’s create a new method, drawAttribute
and use that to drive out the picture from our health attribute. I’m gonna need this:
function Entity:healthAttribute()
return self._health
end
Until now we were just pulling value from the HA.
self:newLine(2)
self:drawText("Health")
self:drawAttribute(m:healthAttribute())
self:newLine()
self:drawText("Speed")
self:drawBarFraction(self.speedIcon, m:speed(), 20)
This might be something like I want. I’m spiking here and am prepared to ditch this when I’ve learned something.
function AttributeSheet:drawAttribute(attribute)
local view = AttributeView(attribute)
view:draw()
end
That seems obvious, as does this:
AttributeView = class()
function AttributeView:init(attribute)
self.attr = attribute
end
function AttributeView:draw()
end
Now we’ll copy the drawing code from the bar fraction thingie:
function AttributeView:draw()
pushStyle()
tint(255)
spriteMode(CENTER)
rectMode(CORNER)
pushMatrix()
translate(80,10)
icon:draw()
popMatrix()
stroke(255)
fill(150,150,150)
rect(100,0,120,20)
--fill(255,0,0)
rect(100,0,120*currentMax/20,20)
fill(255,255,0)
rect(100,0,120*current/20,20)
popStyle()
end
Now we don’t have an icon, or the values current and currentMax (now nominal). Should an attribute know its icon? What about the name? We’ll set that aside for now, and pass the icon in.
self:drawAttribute(self.healthIcon, m:healthAttribute())
function AttributeSheet:drawAttribute(icon,attribute)
local view = AttributeView(icon, attribute)
view:draw()
end
function AttributeView:init(icon, attribute)
self.icon = icon
self.attr = attribute
end
And now reference the things:
function AttributeView:draw()
pushStyle()
tint(255)
spriteMode(CENTER)
rectMode(CORNER)
pushMatrix()
translate(80,10)
self.icon:draw()
popMatrix()
stroke(255)
fill(150,150,150)
rect(100,0,120,20)
--fill(255,0,0)
rect(100,0,120*self.attr:nominal()/20,20)
fill(255,255,0)
rect(100,0,120*self.attr:value()/20,20)
popStyle()
end
This works as advertised. Here are two pictures, one showing Princess’s health below nominal, and one above. I don’t like the above:
You can see the yellow line in the first picture, showing her nominal health value. When we implement incremental healing, she’ll heal back up to that point. But when she picks up health items, they can bump her health up above that point, and then it’ll drift back down to nominal.
So I’d like that line to be visible all the time. I think we accomplish that by drawing it last, in a contrasting color. Right now, it’s actually a rectangle in white. Maybe that’ll be good enough if we move it to last. I’ll look and see. No. White on yellow not so good.
What’s to do. Black?
Well, that’s visible but it makes the border look jagged. We could draw in the reverse order, bottom up, current, nominal, boundary, if we draw the boundary with no fill. Let’s try that.
function AttributeView:draw()
pushStyle()
tint(255)
spriteMode(CENTER)
rectMode(CORNER)
pushMatrix()
translate(80,10)
self.icon:draw()
popMatrix()
stroke(255)
fill(150,150,150)
rect(100,0,120,20)
fill(255,255,0)
rect(100,0,120*self.attr:value()/20,20)
noFill()
stroke(0)
rect(100,0,120*self.attr:nominal()/20,20)
stroke(255)
rect(100,0,120,20)
popStyle()
end
This looks acceptable for now:
Let’s commit this and see where we stand. Commit: New AttributeView class used for HealthAttribute.
Where Do We Stand?
We stand in a fairly nice place. We have a better display of the HealthAttribute than we had before, and we have a new object, an AttributeView, that does the job. I think it is fairly clear that when we finally get our “character sheet” or whatever it is, that we’ll pass that to the AttributeSheet (which is really no more than a big view anyway) instead of passing the whole Entity. So we’ll be reducing coupling substantially. This is good. How about using this View for the other two graphs? Can we get there from here?
Our View wants to send two messages to its parameter, nominal()
and value()
. We don’t really have anything quite like that to hand. We could, however, cheat.
Hold my chai:
function AttributeSheet:draw()
local m = self.monster
if not m:displaySheet() then return end
pushMatrix()
pushStyle()
resetMatrix()
zLevel(10-m:distanceFromPlayer())
rectMode(CORNER)
textMode(CORNER)
textAlign(LEFT)
self:drawParchment()
fill(0)
self:drawText(m:name())
self:newLine(2)
self:drawText("Health")
self:drawAttribute(self.healthIcon, m:healthAttribute())
self:newLine()
self:drawText("Speed")
self:drawAttribute(self.speedIcon, HealthAttribute(m:speed(), 20))
self:newLine()
self:drawText("Strength")
self:drawAttribute(self.strengthIcon, HealthAttribute(m:strength(), 20))
self:drawKeys()
self:drawPhoto(m:photo())
popStyle()
popMatrix()
end
We just wrap our strength and speed values in an HA for the nonce, and then they draw just fine. We can now remove the drawBarFraction
method from AttributeSheet, as it is now fully delegating to the AttributeView.
Everything works. It’ll work better when we use real attributes for strength and speed, as they’ll have nominal values to show. We’ll of course make our power-ups decline down to nominal, similarly to what we’ll do with health.
Commit: AttributeView used for strength, speed, and health.
Let’s Sum Up
Things are starting to improve rather visibly. We’ve reduced complexity in Entity a bit, though its interface hasn’t improved yet, because we’re still forwarding from Entities to their HealthAttribute. That may change later on but a forwarding method is a lot simpler than a method that does something. All the doing is down in the Attribute.
Instead of teaching the Attribute to draw itself, we created an AttributeView. That’s perhaps the first time I’ve used that pattern in Codea, because the programs always seem so small as not to warrant it. Or because I’m not smart enough. Or because I’m too lazy.
Hold on there!
I’m being “amusing”1. There is no value to beating ourselves up when our code isn’t as good as it might be. Our code is not ourselves, it is merely a thing that we made. We are human, and we make mistakes. We make coding mistakes, and we make planning and design and labor mistakes. We are constantly trying to find a balance between all the forces on us, the need for speed, the need for correctness, the need for fun, the need for food.
Often, probably very often, we don’t reach the balance that, in retrospect, we might have wished. In our line of work, programming, when we make a mistake we don’t get cut or smash our thumb or cut off the tip of a finger. Our mistakes are pretty light in weight.
There’s no point shouting at ourselves, denigrating ourselves, beating ourselves up. The thing is this:
Look how much better this is now, and I did it with the help of my friends!!!
We can just see a glimmering of how this is going, and it is already making things better. I foresee that it’s going to continue to improve the code, and thereby make the product easier to build, and that new capabilities will be able to be added faster.
A very good day. Who knew? Apparently Beth did. I thank hir for hir idea, and take all the credit for myself.
-
We at the FBI do not have a sense of humor we’re aware of. ↩