You know that thing where they ask you for something no one ever thought they’d ask for? They just asked for it.

On real projects, it’s the moment we all dread. One day, out of the blue, “they” come in and ask for something that the system’s design Just Can’t Do. We explain to “them” that our software Just Can’t Do That. They pretend to pay attention, and then tell us they need it by Thursday, or two weeks out, or whatever. The date doesn’t matter: the software Just Can’t Do That.

Here in these articles, these are the days I live for. Why? Because I believe that when we have a really good design for what the system does right now, that design will allow us to build anything they ask for, at a reasonable cost. When that happens, I tell you about it, and I hope you’re motivated to work toward the kinds of designs and code you see here, and that you discover for yourself that you don’t have to get buried in code that gets worse and worse the longer you live in it.

It happened last night, during the Zoom Ensemble. We mostly spoke of things other than code last night, and we did’t actually look at any code, but at one point I wanted to talk about something that had come to mind.

Chet Hendrickson has the habit of trying to remove if statements from object-oriented code. See an if, kill it, seems to be his motto. My own practice isn’t that strict, and if the code seems to call for an if, I’ll put it in. I use other measures to keep code clean and to avoid lots of ifs and nesting. I’m not saying that’s better, nor that it’s worse. It’s just different.

So I wanted to get Chet’s opinion. I described the Encounter coroutine to the group, pointing out just a few details, namely that it needs to do things like compare a die roll for the monster against a die roll for the player, and make a determination of a result based on that comparison. Sounds like an if to me.

Oh, there are other ways. You could subtract the two rolls, take the absolute value, and select an action from a table of three functions for monster faster, equally fast, player faster. And so on. That would get rid of the if, but it’s the sort of thing one might do if one were an if hater, but maybe not if you could understand ifs.

Chet said something to the effect that he didn’t mind the one if-else so much but when you started needing to nest them, things got messy quickly. The notion of a case-switch came up.

Then came the question: while the player is having an encounter with the spider, what is the snake doing? Constant readers may recall that right now, the snake is doing nothing. It runs its animation, but it doesn’t move. Neither does the player, or the monster in the fight. The system locks out all motion until the encounter is resolved.

You can see where this is going, I’m sure. The Product Owner, in consultation with the Zoom Ensemble panel of gaming experts, realized that the two-party encounter just won’t do. If a room has multiple monsters in it, they’re not going to line up waiting for their shot at the title. They’re going to pile on.

There’s our new requirement: if there are multiple monsters around, they can all get into battle with the player at the same time. The details are left unspecified for us game developers to figure out, but the fundamental notion was made clear:

If the princess is battling the spider, the snake can slither up and bite her on the butt.1

Our system Just Can’t Do That. The encounter is a coroutine that orchestrates a single encounter between attacker and defender, yielding messages to be displayed int the crawl above the player. At present, neither the monster nor the player has control over how the encounter goes. It’s all down to rolls of the dice and the simple flow through the encounter logic.

And until it’s over, which takes seconds, no other monster can even move, beyond its wriggling or flying animation, much less attack.

And of course, this new monster pile on the princess idea comes along with some other requirements that we’ve talked about in the past:

  • Monsters might have different behavior, such as just wandering unless you bother them;
  • Monsters might be more or less aggressive;
  • The player needs to have more control during an encounter, including selection of whether to fight or try to flee, what weapon to use, perhaps to cast a spell or a grenade.

The first two, we could surely manage within our current structure, since the monsters have two behaviors now, random wandering and moving toward the player, depending on how close they are.

And the latter, it seemed to me could be handled by popping up a graphical menu of things the player can do, prior to the actual encounter, and letting them touch buttons saying how to behave. This would be quite analogous to turn-based combat in D&D and similar games: when it’s the player’s “turn”, they select actions, then the dice roll and an outcome is determined.

It’s “easy” to see that we could just pop up that menu at the beginning of a fight, and that we could pop it up again at the end of an Encounter, if both player and monster are still alive.

But a third entity in Encounter? That seems impossible. Let’s review the code:

Encounter

local yield = coroutine.yield

function createEncounter(player,monster,random)
    f = function()
        attack(player, monster, random or math.random)
    end
    return f
end

function attack(attacker, defender, random)
    yield(attacker:name().." attacks ".. defender:name().."!")
    local attackerSpeed = rollRandom(attacker:speed(), random)
    local defenderSpeed = rollRandom(defender:speed(), random)
    if attackerSpeed >= defenderSpeed then
        yield(attacker:name().." is faster!")
        firstAttack(attacker,defender, random)
    else
        yield(defender:name().." is faster!")
        firstAttack(defender,attacker, random)
    end
end

function firstAttack(attacker,defender, random)
    yield(attacker:name().." strikes!")
    local attackerSpeed = rollRandom(attacker:speed(), random)
    local defenderSpeed = rollRandom(defender:speed(), random)
    if defenderSpeed > attackerSpeed then
        attackMisses(attacker,defender, random)
        if math.random() > 0.5 then
            yield("Riposte!!")
            firstAttack(defender,attacker, random)
        end
    else
        attackStrikes(attacker,defender, random)
    end
end

function attackMisses(attacker, defender, random)
    yield(defender:name().." avoids strike!")
end

function attackStrikes(attacker,defender, random)
    local damage = rollRandom(attacker:strength(), random)
    if damage == 0 then
        yield("Weak attack! ".. defender:name().." takes no damage!")
        if math.random() > 0.5 then
            yield("Riposte!!")
            firstAttack(defender,attacker, random)
        end
    else
        defender:displayDamage(true)
        yield(attacker:name().." does "..damage.." damage!")
        defender:displayDamage(false)
        defender:damageFrom(attacker.tile, damage)
        if defender:isDead() then
            yield(defender:name().." is dead!")
        end
    end
end

The Encounter is, by its nature, a two-combatant thing. It considers an attacker and a defender and just runs its flow chart until a result comes out. Along the way, it can call firstAttack recursively, often with defender and attacker reversed, to allow for ripostes and one entity being faster than the other on this roll.

To exacerbate the problem, the Encounter is actually driven by the Crawl, which scrolls up at some speed, and when it wants a new line of info, it calls the encounter, which runs up to its next yield statement:

function Floater:fetchMessage()
    if coroutine.status(self.provider) == "dead" then return end
    local tf, msg = coroutine.resume(self.provider)
    if tf then
        if msg ~= nil then table.insert(self.buffer, msg) end
    else
        print("error: "..msg)
        table.insert(self.buffer, "error: "..msg)
    end
end

This is the situation in which we find ourselves. We need to look at our options.

Options

We could just stop this series at number 79, saying, well, we’ve proven that we can write a dungeon game, nothing to see here, move along.

That doesn’t seem quite right.

Bryan offered an idea during the Zoom Ensemble, which was why couldn’t we create an Encounter for each monster-player pair, and maybe put some kind of encounter-summary object on top?

That idea immediately appealed to me, in that it preserves the two-party encounter, of which I am perhaps unduly proud, yet it could certainly provide the ability for the various monsters to join in the fray. Thinking about it now, you can even see how a turn-based thing could fit right in, just running the remaining live monster encounters one after another.

It seems like the top-level summary object could be checking which coroutines were dead, which would imply that that monster was dead or had fled the scene. (And, like the player, they should have the option to try to flee if things are getting tough.) The top-level summary thing would just stop calling that coroutine, continuing the encounter until the final outcome is determined.

This feels to me to be possible. I think there are issues, such as what the Floater (the object that operates the crawl) will do if it doesn’t get something new to draw. Looking at its code, I think the game will lock up:

function Floater:draw()
    local n = self:numberToDisplay()
    if n == 0 then return end
    pushStyle()
    fill(255)
    fontSize(20)
    local y = self:yOffset()
    local pos = self.runner:playerGraphicCenter()
    pos = pos + vec2(0,self.yOff)
    for i = 1,n do
        text(self.buffer[i], pos.x, pos.y)
        pos = pos - vec2(0,self.lineSize)
    end
    self:increment()
    popStyle()
end

It’s calling increment during draw, and increment can call fetchMessage and that resumes the coroutine. If the princess’s popup menu were made part of the coroutine, it might not return until she made a selection, which could be a long time compared to the 60th of a second we have between draw calls.

So, I don’t know, we’d have to deal with that. Maybe it returns “I’m thinking”. Maybe all the turns are taken outside the encounter, which would just handle one cycle of turns.

So if Bryan’s Idea pans out, maybe we’re not doomed. In that case, it will be renamed to Ron’s Idea. If it doesn’t pan out, it will remain Bryan’s Idea.

But to make a decision, we should consider other alternatives. What are some of them?

Alternatives

Change the Crawl

There’s the issue of the crawl, which is intended to be like a console, listing useful information like the fact that you’ve found a strength gem, or that you’ve been assailed by a snake. Our crawl, because it crawls, wants to pull information. It doesn’t really want to have information pushed at it.

Maybe we could make the crawl be more of a console, where you push info rather than have it pulled. That might simplify things, since we could probably get rid of the coroutine.

However, if we’re going to do turn-based, we’ll need some kind of hand-off mechanism between monster and player, and a coroutine relationship might still be best.

Coroutines are a bit deep in the bag of tricks, though, and we might be wise to avoid them. Certainly making things more complicated would be bad if we can avoid it.

Change Combat

I probably should say “figure out combat”. If we want it to be somewhat turn-based, we’ll need to understand a new “flow chart”, amounting to a coding of the D&D-like rules we pick into the behavior of monsters and the player, with the added issue of the player’s actions not being able to be demanded when we want them, as we can do with the monsters.

There can be little doubt that a more turn-based approach will change the current logic, since it manages an arbitrary number of turns between defender and attacker now, and, presumably, our new scheme should manage only one turn at a time.

Something Really Simple

Setting aside the reporting / crawl / console issue, maybe the whole thing can be made very simple, simpler than it is now. Presently, monsters move at random intervals, depending on the monster. Basically they move every 1.0 to 1.5 seconds, randomly. Right now, when they move, they choose to move randomly or toward the princess:

function Monster:chooseMove()
    if self.runner:monsterCanMove() then
        if not self.alive then return end
        if self:distanceFromPlayer() <= 10 then
            self:moveTowardAvatar()
        else
            self:makeRandomMove()
        end
    end
    self:setMotionTimer()
end

They could have other behaviors, such as patrolling, that would be plugged in here. Who knows, maybe a strategy object. That would be fun.

When a monster tries to enter the player’s tile, it triggers the encounter. A fair amount of stuff happens, but a TileArbiter will come into being, pairing the princess and the monster, looking up the behavior desired in the TA table:

    t[Player][Monster] = {moveTo=TileArbiter.refuseMove, action=Monster.startActionWithPlayer}

This says “can’t move in, but start action. Monster does this:

function Monster:startActionWithPlayer(aPlayer)
    if aPlayer:isDead() then return end
    self.runner:runCrawl(createEncounter(self,aPlayer), true)
end

An Encounter is created and the crawl pulls the behavior from it, resulting in the little battle story scrolling up.

While the crawl is running, all action stops. If we didn’t do this, then the monster could attack again and again or the princess could start to run away, but the crawl still would be running the Encounter, which looks odd.

But maybe we could just do one quick interaction and be done with it. Monster whacks at Player, hits and does damage, or misses. Player’s turn.

But even here, we have to stop at least this monster from taking any more action. We’ll have to decide whether it’s right to stop all of them: the Product Owner thinks not, the others should be able to run up and pile on.

Maybe when you initiate an attack, you get put into a table of combatants, and that table freezes them all for anything other than battle. So if the snake comes up from behind, it gets added to the combatant table and starts taking turns.

More study

It’s clear that we need to think more about what we want than how to do it. As we talk about each of the topics here, it seems like our basic objects can support whatever we want to do, perhaps at the cost of a “battle state” in all entities, and that we have at least a couple of alternate ways to go.

But today, for this hour, we don’t have any code to write, at least not for Encounter. We’ll need to figure out a plan, and I think we need to read up on combat cycles.

I do have one thing in mind to code, however.

hornet

In the picture above, the Murder Hornet is visible on screen, and his attribute sheet is visible, even though he is not in view. I think that neither of those should happen. If we had ominous music, we might play it, but showing the monsters when they’re not in view is weird and gives the player more info than they should have.

How is this happening, and what can we do about it?

function Monster:draw(tiny)
    if tiny then return end
    self:drawMonster()
    self:drawSheet()
end

function Monster:drawMonster()
    pushMatrix()
    pushStyle()
    spriteMode(CENTER)
    noTint()
    local center = self.tile:graphicCenter()
    translate(center.x,center.y)
    self:flipTowardPlayer()
    if self:isDead() then
        tint(0,128,0,175)
        sprite(self.dead)
    elseif self.showDamage then
        self:selectDamageTint()
        sprite(self.hit,0,0)
    else
        sprite(self.moving[self.movingIndex], 0,0)
    end
    popStyle()
    popMatrix()
end

function Monster:drawSheet()
    if self:distanceFromPlayer() <= 5 then
        self.attributeSheet:draw()
    end
end

We should not draw the monster, nor its sheet, if we’re not displaying the tile. That seems reasonable. In Tile:

function Tile:draw(tiny)
    pushMatrix()
    pushStyle()
    spriteMode(CENTER)
    tint(self:getTint(tiny))
    self:drawSprites(tiny)
    if not tiny then self:drawContents(tiny) end
    popStyle()
    popMatrix()
end

function Tile:drawContents(tiny)
    for k,c in pairs(self.contents) do
        c:draw(tiny)
    end
end

It’s the drawContents that’s doing the drawing of the monsters.

I wonder why chests and loot don’t show up in the dark. Let’s check:

function Loot:draw()
    if tiny then return end
    pushStyle()
    spriteMode(CENTER)
    local g = self.tile:graphicCenter()
    sprite(self.icon,g.x,g.y+10,35,60)
    popStyle()
end

That sure looks like they’d show up, but they don’t. Why not?

I suspect it has to do with tint. The monster draw says noTint() and it sets tint. I think in dark areas, the tint comes in at zero. What if we did this:

function Monster:drawMonster()
    local r,g,b,a = tint()
    if r==0 and g==0 and b==0 then return end

If we do that, the monsters don’t appear. That’a a bit rough, but it works. However, let’s move it up:

function Monster:draw(tiny)
    if tiny then return end
    local r,g,b,a = tint()
    if r==0 and g==0 and b==0 then return end
    self:drawMonster()
    self:drawSheet()
end

Now it should skip over drawing the sheet as well.

hornet1

hornet2

hornet3

In the sequence above, we see that both the hornet and its sheet do not show unless the hornet is within view. So this is good.

Commit: monsters and sheets do not appear when not in view.

That made it work. But interrogating the current tint is a really poor way for decisions to be communicated. Let’s see where that tint gets set.

function Tile:draw(tiny)
    pushMatrix()
    pushStyle()
    spriteMode(CENTER)
    tint(self:getTint(tiny))
    self:drawSprites(tiny)
    if not tiny then self:drawContents(tiny) end
    popStyle()
    popMatrix()
end

function Tile:getTint(tiny)
    --if true and self.runner:playerDistance(self) < 7 then return color(255) end
    if tiny then 
        return self:getTinyScaleTint()
    else
        return self:getLargeScaleTint()
    end
end

function Tile:getTinyScaleTint()
    if self.seen and self.kind == TileRoom then
        return color(255)
    else
        return color(0)
    end
end

function Tile:getLargeScaleTint()
    if self.tintColor.r > 0 then
        return color(255)
    else
        return color(0)
    end
end

This is a pretty roundabout way to be setting tint, since we no longer do the faded tinting trick, with our new darker tiles. So let’s keep in mind that this needs improving. And where is tintColor being set?

function Tile:clearTint()
    self.tintColor = color(0,0,0)
end

function Tile:setTint(aColor)
    self.tintColor = aColor
    if aColor.r > 0 then self.seen = true end
end

function Tile:illuminateLine(dx,dy)
    local max = 8
    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 > max then break end
        local t = math.max(255-d*255/max,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

There we go. Now we don’t really want this tint color thing going on at all. We want a tile to have these states, I think:

  • seen = false. Tile has never had light shone upon it. It paints black everywhere.
  • seen = true. Tile has seen light at some point. It shows white in the tiny map if it is type Room.
  • not presently visible. Tile may or may not have been seen in the past, but it can’t be seen now.

That latter notion is what we’re encoding in looking at tint, which isn’t even used as a tint any more.

What illuminateLine probably ought to do is to send a message to the Tile telling it that it’s illuminated, and leave it up to the tile to decide what to do. If we wanted to do the variable lighting thing, we could pass along the distance from the player, which is our definition of the light source.

To sort this out, we’ve got a few threads to unweave. Let’s see if we can do it in small safe changes.

First, instead of computing and setting the tint, let’s just call a new method:

function Tile:illuminateLine(dx,dy)
    local max = 8
    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 > max then break end
        local tile = self.runner:getTile(pos)
        tile:setVisible(d)
        if tile.kind == TileWall then break end
    end
end

Now:

function Tile:setVisible()
    self.currentlyVisible = true
end

And I think we want this:

function Tile:drawSprites(tiny)
    local center = self:graphicCenter()
    if tiny then
        self:drawMapCell(center)
    elseif self.currentlyVisible then
        self:drawLargeSprites(center)
    end
end

Now this is going to keep things displayed that are out of view. We’ll check that, but then we have to figure out where we can clear this flag. Somewhere before illuminate, of course.

Curiously, the code as written displays nothing at all. Is setVisible not getting called? I check and it is being called.

Oh, I bet we have a problem fetching a black tint. For now …

function Tile:setVisible()
    print("setVisible")
    self.currentlyVisible = true
    self:setTint(color(255))
end

This has an effect I did not anticipate. The screen is never cleared in areas that are not visible:

uncleared

We can’t just exit, we have to draw something. Meh.

I’ll push this a bit further, but it’s not feeling like a refactoring, so we’ll probably revert and take a new aim.

function Tile:drawSprites(tiny)
    local center = self:graphicCenter()
    if tiny then
        self:drawMapCell(center)
    else
        self:drawLargeSprites(center)
    end
end

function Tile:drawLargeSprites(center)
    for i,sp in ipairs(self:getSprites(self:pos(), tiny)) do
        pushMatrix()
        pushStyle()
        translate(center.x,center.y)
        if not self.currentlyVisible then tint(0) end
        sp:draw()
        popStyle()
        popMatrix()
    end
end

If we ever manage to get currentlyVisible turned off, then tiles that can’t be seen will be painted in black. That might be better. What I expect now is that things that are around corners, but that have been illuminated in the past will remain visible.

better

It works better than that. I think someone is still finding a dark tint and setting it. Yes, probably this:

function Tile:draw(tiny)
    pushMatrix()
    pushStyle()
    spriteMode(CENTER)
    tint(self:getTint(tiny))
    self:drawSprites(tiny)
    if not tiny then self:drawContents(tiny) end
    popStyle()
    popMatrix()
end

We’ll nuke that.

too much

Now we get the effect I expected. Note the hallway going off in the upper right. I looked down there, then ran down to the corner. Those tiles should no longer be visible.

Now where can we clear currentlyVisible? We can’t really do it just before we draw, because we only illuminate when the Player moves:

function Player:moveBy(aStep)
    if not self.runner:monsterCanMove() then return end
    self.runner:clearTiles()
    self.tile = self.tile:legalNeighbor(self,aStep)
    self.tile:illuminate()
end

Can we clear all the currentlyVisible flags right there in clearTiles? Probably. What does that do now? Perfect!

function GameRunner:clearTiles()
    for i,row in ipairs(self.tiles) do
        for j,tile in ipairs(row) do
            tile:clearTint()
        end
    end
end

We’ll just clearVisible instead:

function Tile:clearVisible()
    self.currentlyVisible = false
end

This ought to work. Er, but there are other calls to clearTint, and I removed the method. Where are those?

function Tile:init(x,y,kind, runner)
    self.position = vec2(x,y)
    self.kind = kind
    self.runner = runner
    self.contents = DeferredTable()
    self.seen = false
    self:clearTint()
    self.tile = nil
end

Oh, yeah. What I’d give for an editor with better refactoring. Anyway:

function Tile:init(x,y,kind, runner)
    self.position = vec2(x,y)
    self.kind = kind
    self.runner = runner
    --sprite(asset.documents.Dropbox.Torch_0)
    self.contents = DeferredTable()
    self.seen = false
    self:clearVisible()
    self.tile = nil
end

Now it will surely work. The tiles now display OK, but of course the contents are not conditioned by currently visible yet. Let’s hunt those down.

function Tile:draw(tiny)
    pushMatrix()
    pushStyle()
    spriteMode(CENTER)
    self:drawSprites(tiny)
    if not tiny then self:drawContents(tiny) end
    popStyle()
    popMatrix()
end

I think we can just not draw the contents if we’re not visible:

function Tile:draw(tiny)
    pushMatrix()
    pushStyle()
    spriteMode(CENTER)
    self:drawSprites(tiny)
    if not tiny or not self.currentlyVisible then self:drawContents(tiny) end
    popStyle()
    popMatrix()
end

For some reason I’m still seeing some contents and the monsters. The monsters of course need to check the new situation.

function Monster:drawMonster()
    if not self.tile.currentlyVisible then return end
    pushMatrix()
    pushStyle()
...

That works. But why is the chest drawing at all? With an assert, I find:

Chest:17: how did we get here
stack traceback:
	[C]: in function 'assert'
	Chest:17: in method 'draw'
	Tile:149: in method 'drawContents'
	Tile:142: in method 'draw'
	GameRunner:177: in method 'drawMap'
	GameRunner:166: in method 'drawLargeMap'
	GameRunner:145: in method 'draw'
	Main:30: in function 'draw'

Tile has called drawContents. I must have done something wrong:

Yes, right:

function Tile:draw(tiny)
    pushMatrix()
    pushStyle()
    spriteMode(CENTER)
    self:drawSprites(tiny)
    if not tiny or not self.currentlyVisible then self:drawContents(tiny) end
    popStyle()
    popMatrix()
end

Man is not a logical being. Man is a being that can sometimes do logic.

function Tile:draw(tiny)
    pushMatrix()
    pushStyle()
    spriteMode(CENTER)
    self:drawSprites(tiny)
    if not tiny and self.currentlyVisible then self:drawContents(tiny) end
    popStyle()
    popMatrix()
end

And that does it, all the items show when they should and not when they shouldn’t. Now we can look for all the weird references to tint and remove them. But first let’s commit this, since it works: Drawing uses new currentlyVisible flag.

Now for the references to tint. Some are still legit, like the ones that tint monsters when they are dead or hurt. Others, not so much.

Arrgh, there are tests doing getTint. I’m not dead certain that they’re even running, but they’ll need fixing before we’re done here. What else?

It looks to me as if people using getTint are getting some good out of it, because the small map tints black and white according to one scheme (show seen room tiles), and the big map according to another (which should now just be always show, since we just don’t call if we’re not visible).

But this seems safe:

function Tile:getLargeScaleTint()
    if self.currentlyVisible then
        return color(255)
    else
        return color(0)
    end
end

Let’s test. Game seems ok. Tests are failing. Here’s one:

1: Create Health Loot  -- Actual: Health, Expected: Strength

That seems odd. I’ll bet that has failed for a while. Yes, that’s a bug in the test. Was checking for Strength.

No other tests are failing. Let’s continue the search for tint access.

This is weird:

function Tile:setTint(aColor)
    self.tintColor = aColor
    if aColor.r > 0 then self.seen = true end
end

function Tile:setVisible()
    print("setVisible")
    self.currentlyVisible = true
    self:setTint(color(255))
end

We had to set the tint in setVisible but now we should be able to do without it, I think. We’ll try. But now I know some tests will fail. But if the game is good, and the tests fail, the tests are bad.

When I remove that line, the tests do fail as expected, but the mini-map also vanishes. We need to see why. In addition, I think setVisible should set seen to true, and just about no one else should set it. I’m going to risk changing hats for the moment it takes to do that.

function Tile:drawMapCell(center)
    if self.kind ~= TileRoom or not self.seen then return end
    pushMatrix()
    pushStyle()
    rectMode(CENTER)
    translate(center.x, center.y)
    fill(255)
    rect(0,0,TileSize,TileSize)
    popStyle()
    popMatrix()
end

This looks OK. I’m not clear why it didn’t work before but if we set seen it surely should, unless someone is tinting who shouldn’t be.

Yes, game looks good. Does anyone else call setTint, or can I remove it? No other callers. Gone.

Searching for more tint( calls.

It all looks legit. The tints all basically return constants, No reference to tintColor. Now for those tests. They’re all running, the current simplistic tile coloring approach works for them. We could perhaps simplify that code but let’s commit first: tile tint concept simplified using seen and currentlyVisible.

There are no calls to the getTintfunction other than tests. They are in the ignored test that checks for tint as a function of distance. We don’t do that any more. That can go, and so can the tint functions.

Commit: remove last of the tile tint setting logic. This last move saves us 25 lines of code and 17 of test. Nice.

Let’s wrap.

Wrap

Mostly we filled our minds with questions and possible answers today, as we begin to figure out how to deal with a more robust form of combat. I feel optimistic that we won’t have to sacrifice any princesses to get it done, but frankly don’t have a plan yet.

Then we went after a small defect, monsters being visible when they shouldn’t be, and that led us to a cleaner solution for visibility, allowing us to remove a fair bit of nasty code that checked for tint instead of whether a thing is visible.

However, I do observe that there are if statements in the drawing logic and if Chet were looking at the code, he might object. We should consider having a look at whether there’s a better way to manage that logic. I’d bet that there are other ways, and perhaps even better ones to be found.

For today, though, we’re good. It’s Saturday, after all!

See you next time!


D2.zip

  1. This requirement has been edited for content without changing the essential meaning.