Dungeon 94
Happy Valentine’s Day. In other news, I have an idea.
While I wait for my sweetie to get up, I thought I’d try an idea that I had while trying to figure out what to do with my hair. I’m sure it has been at least a year since I’ve had a haircut, and unless I do it, or my honey does, I can see another six months or more without. But I digress.
The idea is this: We would like for the Princess to take her turn, which could in principle involve more than one screen touch or keyboard stroke, and then for each monster to take its turn, one after another. The latter happens now, as the system loops over monsters when it thinks it is monster time. And now a combat round is a single event, so that if the monsters do attack in the same turn, we’ll see each strike separately.
I think I’ll see if I can demonstrate this in the game.
It’s a shame that we had to harm a princess to get the above movie, but I assure you that she’s only mostly dead. We see in the final sequence that first the vampire bat attacks and completes its attack, and then the pink slime attacks and finishes off the brave, not to say intrepid, princess.
The main issue we’re trying to deal with is that if a combat round kills a monster, its move may have already been scheduled, so the dead monster seems to attack the princess. This is awkward, and frankly more than a bit naff. We do have a flag that supposedly controls when monsters can move. We may have vestiges of other mechanisms as well. Let’s have a look at the code.
Do They Go? Are They Goers?
GameRunner, when it creates a level, initializes the flag playerCanMove
to true
. This information can be accessed by GameRunner’s collaborators. Internal references are:
function GameRunner:itsPlayerTurn()
return self.playerCanMove
end
function GameRunner:stopFloater()
self.playerCanMove = true
end
function GameRunner:startFloater()
self.playerCanMove = false
end
function GameRunner:turnComplete()
--self.playerCanMove = false
self:moveMonsters()
--self.playerCanMove = true
end
We note that turnComplete used to toggle the flag and the toggling was commented out. At this moment I’d have to read some past article to figure out why. And you can be sure that I’m not going to do that. That’s why folks who say that decisions should be documented somewhere may be mistaken, because no one ever looks somewhere.
I wonder whether startFloater and stopFloater are even used, since the Floater never stops now. I’ll check.
function Floater:startCrawl(listener)
self.listener = listener or FloaterNullListener()
self.yOff = self.yOffsetStart
self.buffer = {}
self.listener:startFloater()
self:fetchMessage()
end
function Floater:increment(n)
self.yOff = self.yOff + (n or self:adjustedIncrement())
if self:linesToDisplay() > self.lineCount then
table.remove(self.buffer,1)
self.yOff = self.yOff - self.lineSize
end
if #self.buffer < self:linesToDisplay() then
self:fetchMessage()
end
if #self.buffer == 0 then
self.listener:stopFloater()
end
end
We learn some things.
First, these are really bad names. They should be “floaterIsStarting” and “floaterIsStopping”. As written they sound like commands and they are reports of Floater activity.
Second, start will only ever be sent once, when the Floater is initially fired up.
Third, every time we detect buffer empty we’ll send the stop … but …
function Floater:fetchMessage()
local msg = self.provider:getItem()
table.insert(self.buffer, msg)
end
function Provider:getItem()
if #self.items < 1 then return self.default end
...
end
The buffer will never be empty at that point, because we now run the Floater all the time, scrolling blank lines.
Now does anyone use that itsPlayerTurn
method? I think yes:
function Player:itsOurTurn()
return self.runner:itsPlayerTurn()
end
function Button:shouldAccept(aTouch, player)
return aTouch.state == BEGAN and player:itsOurTurn() and self:mine(aTouch)
end
function Player:keyPress(key)
if not self:itsOurTurn() then return false end
self:executeKey(key)
self:turnComplete()
end
So buttons and keys will not function if that flag is set false. And at present it never is.
I think that player’s ability to move does need to be constrained, as she can wander around in a battle, which isn’t quite what’s intended. But this is separate from the question of when the monsters move. That’s done when this is called:
function GameRunner:turnComplete()
--self.playerCanMove = false
self:moveMonsters()
--self.playerCanMove = true
end
And that’s called here:
function Button:performCommand(player)
player[self.name](player)
player:turnComplete()
end
function Player:keyPress(key)
if not self:itsOurTurn() then return false end
self:executeKey(key)
self:turnComplete()
end
As soon as the key or button is processed, we’re freeing the monsters to move. That’s too soon, as we’ve seen when dead ones attack because they don’t know that they will have been dead when the round actually starts.
If we can arrange that turnComplete
is only sent when the crawl runs dry, I think that will enforce monsters not running when they shouldn’t. And, finally, the idea:
Since these two points that presently call it are reached after all the button or key actions have been taken, if they were to add a deferred turnComplete
to the crawl, instead of sending it directly, we could be sure that anything the game has already sent into the crawl, like a combat round, will be complete before turnComplete
is sent.
If this works, we should be almost perfectly turn-based.
So let’s do it. Both these methods hit player’s turnComplete
, which, again, is this:
function Player:turnComplete()
self.runner:turnComplete()
end
If this enqueues a turnComplete instead, I think we’ve solved the problem.
function Player:turnComplete()
local op = OP("extern", self.runner, "turnComplete")
self.runner:addToCrawl( { op } )
end
Fairly extensive game play gives me confidence that this works: I see many back and forth exchanges, but none from dead creatures. I’m not certain how to actually test that a thing like this cannot happen, since we can imagine all kinds of ways that it might happen, but since you can’t get a monster turn until player sends turnComplete
to the crawl, we can see that it’s isolated, if not proven.
We will someday have an issue. When we come up with player actions that require multiple keystrokes or button presses, some buttons will send turnComplete
and some not. We’ll deal with that when it comes, but I can see already that it may become a source of errors. When it comes, I’ll try to remember that concern and deal with it in some sensible way.
Let’s commit: turnComplete
now issued at end of crawl. Resolves monsters attacking when dead.
Now one more thing: I noticed that when a monster attacks right after the player attacks, the text follows right on in the crawl, as you’d expect. It would be better if there were a blank line. I think we can do that like this:
function CombatRound:attack()
if self.attacker:isDead() then return {} end
local result = {}
table.insert(result, self:display(" ")) -- <---
local msg = string.format("%s attacks %s!", self.attacker:name(), self.defender:name())
table.insert(result, self:display(msg) )
local cmds = self:attemptHit()
table.move(cmds,1,#cmds,#result+1, result)
return result
end
That should display a blank line between combat rounds and should be otherwise harmless.
The movie above shows the effect. I like it. Commit: Blank line between combat rounds.
We do still have the issue that the princess can move when the crawl is running. I suspect that will be easy to resolve, but I’m not going after it today. After an important success and a little one, I’m not going to press my luck.
Let’s Sum Up
“Isn’t it ironic” that the thing I’ve been fussing over for so long, getting combat sorted out so that combat lines don’t interleave and dead guys don’t attack, has turned out to be so simple? I honestly don’t know: I’ve never understood irony.
The Big Fix was doing the whole combat round for just one side, and all in one batch. That was nearly trivial to do and resolved the interleaving entirely.
The Small Fix, today’s, was to enqueue an indication that the turn was over, so that it only triggers when the crawl thinks everything is done. I had considered causing the Floater and/or Provider to send the message upon buffer empty (first default generated), but just enqueueing it at the right moment makes more sense. Or seems to.
The concern hanging over me has been whether the current form of combat could bear the weight of the requirements. I am now quite confident that it can. It remains to be proven, by adding in the refinements that I have in mind, but for now, I think we’ve iterated to a good solution.
And that may be the entire point of Agile Software Development:
We iterate to good solutions. We do not expect to get things absolutely right on the first try. We try very hard to get things correct, in the sense of bug-free, all the time, but we are not at all surprised when we discover that part of our solution, while good enough for yesterday, is no longer good enough for tomorrow.
Because we keep our code well-tested and well-structured, we are confident that we’ll always discover that we can replace something that used to be adequate but no longer is, with something that will serve better, without massive editing all over the universe.
However:
It is fair to say that I see some things here to be concerned about. The pace of development here, which has been fast and has involved a number of upgrades to ideas, has left some duplication, some tag ends of code that may no longer even be useful, and at least one, and I think more than one, object whose code layout is far from ideal.
I need to bear down a bit on cleaning things up, on removing duplication, and on making sure that I pull out all the leftover bits when I make changes. Other languages and IDEs might make that easier, but either way, it’s my responsibility to keep the campground in good order.
Keep an eye on that and remind me, OK?
See you next time!