Dungeon 84
More on turn-based. Let’s try converting Encounter to a combat round, or writing a new combat round. Or something else.
It occurred to me yesterday that I’ve written over 80 articles on dungeon alone, in not much more than 80 days, and the articles are surely at least 2000 words each. So easily 160,000 words in three months. I should try writing something for money.
Today, however, I plan to work more on the turn-based logic. In aid of that, I’ve come to recognize a need for events in the program to “force” one or the other of the turns to come to the fore. For example, if an encounter caused the MonsterTurn to come to the fore, the player couldn’t move until the round was over and that’s a feature we want. The monsters wouldn’t move either, because they don’t move during encounters. Probably.
It comes to me now that an even better thing might be to have a NobodyMove turn, and jam that in place in the GameRunner’s turn
variable, and then no one would do anything. And that much speculation is enough to remind me that at the moment I don’t have the need for any of that, nor the context to be sure I understand quite what’s needed.
As I alluded to up in the blurb, I have the notion of simplifying the encounter so that it is just one combat pass, instead of the looping thing that it is now, so that at the end of each turn cycle, the player can choose another action, represented by our current Flee/Fight buttons, which do nothing but represent.
However, there’s always a however, isn’t there? We don’t really want the game to go backward, so instead of reducing the current encounter, let’s create a new one, and remove the old one when we can cut over to the new form of combat. We do that for a few reasons:
- It’s good practice to have the product always getting visibly better, never visibly worse, when it comes to the politics of real-world programming.
- It’s good practice, therefore, to practice doing that in our fun and example programming such as we’re doing now.
- It makes the work a bit more challenging, and that’s good for the articles and for my own learning, and maybe even that of readers.
- Creating the new encounter from scratch will let us avoid any grunginess that is in the old one, though we can still read it and learn from it. Clean sheet of paper sort of thing.
- I want to do it that way.
First, Though …
First, though, I want to get the current encounter rigged to use the new Turn objects, such that during the encounter, the player can’t be banging away on the keys trying to do stuff. In due time, I have in mind dimming the on-screen controls when they are inactive, as a sign to the player to chill out.
Let’s see what the encounter and floater do about trying to stop the action now. When we start a new crawl, we use this code:
function Floater:runCrawl(aFunction, stopAction)
if self:okToProceed() then
self.provider = coroutine.create(aFunction)
self:startCrawl(stopAction)
else
print("crawl ignored while one is running")
end
end
function Floater:startCrawl(stopAction)
self.stopAction = stopAction
self.yOff = self.yOffsetStart
self.buffer = {}
if self.stopAction and self.runner then self.runner:stopMonsters() end
self:fetchMessage()
end
We pass a flag in saying whether we want to stop the action or not. If we do, we send stopMonsters
to the GameRunner. We also have a way of clearing that, here:
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
if self.stopAction and self.runner then self.runner:startMonsters() end
end
end
Now there are things not to like about this “feature”. It sends a very specific message to a very specific object when the crawl starts and stops. That means that this object is rather tightly coupled to the GameRunner. That said, the effect is much like the one we need. But I think we can do better.
Now floater does use the runner
for another purpose, which is to get the position of the player. Even that isn’t really general enough, but for now, we’ll let it slide. Instead of sending in a flag saying do or do not send those specific messages, let’s pass in an object, to which the floater should send more generic messages, perhaps floaterStart
and floaterStop
. If we do not pass an object, the floater will provide one of its own. (That will let us get rid of the if statements.
This should be a fairly straightforward refactoring. We’ll start at the top. We use the floater thusly in GameRunner:
function GameRunner:init()
...
self.cofloater = Floater(self, 50,25,4)
self:createTurns()
end
function GameRunner:createLevel(count)
...
self:runCrawl(self.initialCrawl, false)
self:turnComplete() -- start things going
end
function Monster:startActionWithPlayer(aPlayer)
if aPlayer:isDead() then return end
--Encounter(self,aPlayer):attack()
self.runner:runCrawl(createEncounter(self,aPlayer), true)
end
function Player:addHealthPoints(points)
local msg = string.format("+%d Health!!", points)
local f = function()
coroutine.yield(msg)
end
self.runner:runCrawl(f, false)
self.healthPoints = math.min(20, self.healthPoints + points)
end
function Player:doCrawl(kind, amount)
local msg = string.format("+%d "..kind.."!!", amount)
local f = function()
coroutine.yield(msg)
end
self.runner:runCrawl(f, false)
end
function Player:startActionWithMonster(aMonster)
if aMonster:isDead() then return end
self.runner:runCrawl(createEncounter(self,aMonster), true)
end
function GameRunner:drawMessages()
pushMatrix()
self:scaleForLocalMap()
self.cofloater:draw()
popMatrix()
end
function GameRunner:runCrawl(aFunction, stopAction)
self.cofloater:runCrawl(aFunction, stopAction)
end
Hm, a bit more than I had thought. Let’s do this: we’ll create a new call to GameRunner, runBlockingCrawl
, and use the existing one, without the flag, for non-blocking crawls. For now, we can make those changes using the existing flag scheme:
function GameRunner:runCrawl(aFunction, stopAction)
self.cofloater:runCrawl(aFunction, stopAction)
end
This will become:
function GameRunner:runCrawl(aFunction)
self.cofloater:runCrawl(aFunction, false)
end
And we’ll add:
function GameRunner:runBlockingCrawl(aFunction)
self.cofloater:runCrawl(aFunction, true)
end
Then we’ll “just” edit all the calls in the correct fashion:
function Monster:startActionWithPlayer(aPlayer)
if aPlayer:isDead() then return end
--Encounter(self,aPlayer):attack()
self.runner:runBlockingCrawl(createEncounter(self,aPlayer))
end
function Player:startActionWithMonster(aMonster)
if aMonster:isDead() then return end
self.runner:runBlockingCrawl(createEncounter(self,aMonster))
end
All the other calls used the false
flag so were changed to a plain vanilla runCrawl
.
The game should play as usual. And it does. Commit: add runBlockingCrawl, remove flag from all callers.
This is already an improvement: we’ve stopped using a true/false, and moved to more meaningful messages. Now we can address what’s inside the floater and stop using the flag.
Let’s start up top:
These guys will pass in either nothing or self:
function GameRunner:runCrawl(aFunction)
self.cofloater:runCrawl(aFunction, false)
end
function GameRunner:runBlockingCrawl(aFunction)
self.cofloater:runCrawl(aFunction, true)
end
Like this:
function GameRunner:runCrawl(aFunction)
self.cofloater:runCrawl(aFunction)
end
function GameRunner:runBlockingCrawl(aFunction)
self.cofloater:runCrawl(aFunction, self)
end
Now floater needs to expect an optional object. It can deal with that like this:
function Floater:runCrawl(aFunction, object)
if self:okToProceed() then
self.provider = coroutine.create(aFunction)
self:startCrawl(object)
else
print("crawl ignored while one is running")
end
end
That will go here:
function Floater:startCrawl(stopAction)
self.stopAction = stopAction
self.yOff = self.yOffsetStart
self.buffer = {}
if self.stopAction and self.runner then self.runner:stopMonsters() end
self:fetchMessage()
end
Let’s observe that stopAction
is now either nil or an object, so that the system should still work, since nil acts as false. Let’s test. It’s true, game still runs. We could commit. I guess we should, if we can. Commit: “Floater accepts optional object”.
Now let’s rename and use the object correctly. We’d like to get rid of the if statements. Imagine this:
function Floater:startCrawl(stopObject)
self.stopObject = stopObject
self.yOff = self.yOffsetStart
self.buffer = {}
if self.stopObject and self.runner then self.runner:stopMonsters() end
self:fetchMessage()
end
This is just a rename. I renamed the other occurrences, but now I’m going to do more to each of them:
function Floater:startCrawl(stopObject)
self.stopObject = stopObject
self.yOff = self.yOffsetStart
self.buffer = {}
if self.stopObject and self.runner then self.runner:stopMonsters() end
self:fetchMessage()
end
Let’s get rid of that if check for stopObject by just–well–getting rid of it:
function Floater:startCrawl(stopObject)
self.stopObject = stopObject
self.yOff = self.yOffsetStart
self.buffer = {}
if self.runner then self.runner:stopMonsters() end
self:fetchMessage()
end
This will explode if we run it, but let’s do the other before we forget.
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
if self.stopObject and self.runner then self.runner:startMonsters() end
end
end
That becomes:
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
if self.runner then self.runner:startMonsters() end
end
end
We’re not done with the if statements, but we aren’t checking stopAction any more. Of course now the game will explode …
I’m mistaken. Now it will always send the message to runner, unconditionally. We don’t want that. We want to send the message to the provided object:
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
if self.runner then self.stopObject:startMonsters() end
end
end
function Floater:startCrawl(stopObject)
self.stopObject = stopObject
self.yOff = self.yOffsetStart
self.buffer = {}
if self.runner then self.stopObject:stopMonsters() end
self:fetchMessage()
end
Now it’ll explode:
Floater:85: attempt to index a nil value (field 'stopObject')
stack traceback:
Floater:85: in method 'startCrawl'
Floater:71: in method 'runCrawl'
GameRunner:306: in method 'runCrawl'
GameRunner:87: in method 'createLevel'
Main:18: in function 'setup'
Excellent, my cunning plan is coming together. Since those two if
lines don’t reference runner, we can remove the if parts entirely:
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.stopObject:startMonsters()
end
end
function Floater:startCrawl(stopObject)
self.stopObject = stopObject
self.yOff = self.yOffsetStart
self.buffer = {}
self.stopObject:stopMonsters()
self:fetchMessage()
end
This explodes just as badly, but no worse. We need to fix the exploding.
Stand back a bit, this is just slightly clever.
FloaterResponder = class()
function FloaterResponder:stopAction()
end
function FloaterResponder:startAction()
end
Here’s an object understanding two messages, start and stop action. It ignores them both. We then take this:
function Floater:startCrawl(stopObject)
self.stopObject = stopObject
self.yOff = self.yOffsetStart
self.buffer = {}
self.stopObject:stopMonsters()
self:fetchMessage()
end
And turn it into this:
function Floater:startCrawl(stopObject)
self.stopObject = stopObject or FloaterResponder()
self.yOff = self.yOffsetStart
self.buffer = {}
self.stopObject:stopMonsters()
self:fetchMessage()
end
Then–and I should have done this in two steps, but we’re big kids here, we change the messages sent to align with FloaterResponder, remembering to change GameRunner as well.
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.stopObject:startAction()
end
end
function Floater:startCrawl(stopObject)
self.stopObject = stopObject or FloaterResponder()
self.yOff = self.yOffsetStart
self.buffer = {}
self.stopObject:stopAction()
self:fetchMessage()
end
function GameRunner:startAction()
self.freezeMonsters = false
end
function GameRunner:stopAction()
self.freezeMonsters = true
end
And I expect everything to work again. Somewhat surprisingly, everything does work as before. Of course, the player still isn’t blocked from moving, because that’s now controlled by turns, but that was the case right along.
Commit: Floater uses NullObject pattern for callbacks.
NullObject?
The NullObject pattern is one that I would benefit from using more often, I suspect. The basic idea is as we’ve seen here: instead of using a nil (null) and checking for it, we ensure that there’s always an object in place, and we simply send it whatever messages we have to send, and its behavior is benign enough to allow us to carry on.
In our case, it ignores the messages. This is the most common case. Sometimes a Null Object will return some constant result.
As we saw here in this example, the use of our FloaterResponder allowed us to remove two if statements, at the cost of an or
construct here:
function Floater:startCrawl(stopObject)
self.stopObject = stopObject or FloaterResponder()
self.yOff = self.yOffsetStart
self.buffer = {}
self.stopObject:stopAction()
self:fetchMessage()
end
Now that we have a better sense what we’re doing, how about some renaming up in this thing? What is a good word for the person or thing we respond to? We’re the responder and they are what. Respondent? Meh. InterestedParty? Supervisor? Partner? Ah. Listener. There we go.
And the messages we sent, startAction and stopAction, those imply knowledge of what the listener wants to do. And they’re kid of reversed in that we send stop when we start and start when we stop. How about startFloater and stopFloater? We’ll try that.
function GameRunner:stopFloater()
self.freezeMonsters = false
end
function GameRunner:startFloater()
self.freezeMonsters = true
end
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
FloaterNullListener = class()
function FloaterNullListener:stopFloater()
end
function FloaterNullListener:startFloater()
end
Game runs fine. Commit: rename floater null object to listener, change messages to start and stop floater.
What About the Swamp?
When we came in here, we had in mind working on combat. I specifically remember saying:
More on turn-based. Let’s try converting Encounter to a combat round, or writing a new combat round. Or something else.
Well, this has certainly been in aid of combat, and it was certainly something else, so I guess I wasn’t lying. Let’s see if we can fiddle the new startFloater
stopFloater
messages to stop the player from moving during an encounter. I think this will be a bit tricky, but perhaps not too tricky.
If we could set GameRunner’s turn
variable to MonsterTurn, then the player couldn’t move. Then if we set to PlayerTurn upon end of the encounter, player gets first shot at doing something.
To do that we’ll need a way of setting the turn, not just getting the next turn. Let’s assume we have that:
function GameRunner:stopFloater()
self:setPlayerTurn()
end
function GameRunner:startFloater()
self:setMonsterTurn()
end
That seems right. Of course we don’t know how to do that. How do we set up the turns?
function GameRunner:init()
self.tileSize = 64
self.tileCountX = 85 -- if these change, zoomed-out scale
self.tileCountY = 64 -- may also need to be changed.
self.tiles = {}
for x = 1,self.tileCountX+1 do
self.tiles[x] = {}
for y = 1,self.tileCountY+1 do
local tile = Tile:edge(x,y, self)
self:setTile(tile)
end
end
self.cofloater = Floater(self, 50,25,4)
self:createTurns()
end
function GameRunner:createTurns()
self.turns = {PlayerTurn(self), MonsterTurn(self)}
end
function GameRunner:createLevel(count)
...
self:createCombatButtons(self.buttons)
self:runCrawl(self.initialCrawl)
self.turnIndex = #self.turns
self:turnComplete() -- start things going
end
function GameRunner:turnComplete()
self.turn = self:nextTurn()
self.turn:execute(self)
end
Let’s do this a bit differently. This table indexing stuff is a bit too low-level, relying on indexes and such.
Instead, what if the turns each knew the other and we just ask them who’s next?
function GameRunner:createTurns()
local pt = PlayerTurn(self)
local mt = MonsterTurn(self)
mt:setNext(pt)
pt:setNext(mt)
self.turn = mt
end
function Player:getNext()
return self.next
end
function Player:setNext(aTurn)
self.next = aTurn
end
function Monster:getNext()
return self.next
end
And finally:
function GameRunner:turnComplete()
self.turn = self.turn:getNext()
self.turn:execute(self)
end
And we remove GameRunner:nextTurn
entirely.
I expect this to work. By a narrow margin of “expect”.
GameRunner:155: attempt to call a nil value (method 'setNext')
stack traceback:
GameRunner:155: in method 'createTurns'
GameRunner:19: in field 'init'
... false
end
setmetatable(c, mt)
return c
end:24: in global 'GameRunner'
TestEncounter:18: in field '_before'
CodeaUnit:44: in method 'test'
TestEncounter:25: in local 'allTests'
CodeaUnit:16: in method 'describe'
TestEncounter:10: in function 'testEncounter'
[string "testEncounter()"]:1: in main chunk
CodeaUnit:139: in field 'execute'
Tests:329: in function 'runCodeaUnitTests'
Main:10: in function 'setup'
Oh, that’s a test failing. TestEncounter. Let’s look at that. Yucch, I have no idea what’s up with that, but it was there to test the coroutine. Let’s ignore
and see what else breaks.
Same message and now I recognize what it will be, there’ll be a dot where there should be a colon:
No, I am a total dummy. Look:
function MonsterTurn:isPlayerTurn()
return false
end
function Monster:getNext()
return self.next
end
function Monster:setNext(aTurn)
self.next = aTurn
end
Those methods are defined on Monster, not MonsterTurn. Same with Player. Sheesh.
Now we get an error that makes sense:
GameRunner:86: attempt to get length of a nil value (field 'turns')
stack traceback:
GameRunner:86: in method 'createLevel'
Main:18: in function 'setup'
We’re still checking and setting the turn index, which we no longer use. Remove that line. Run again.
Another sensible message, as we have not yet built this:
GameRunner:336: attempt to call a nil value (method 'setMonsterTurn')
stack traceback:
GameRunner:336: in method 'startFloater'
Floater:85: in method 'startCrawl'
Floater:71: in method 'runCrawl'
GameRunner:308: in method 'runBlockingCrawl'
Player:208: in local 'action'
TileArbiter:27: in method 'moveTo'
Tile:82: in method 'attemptedEntranceBy'
Tile:306: in function <Tile:304>
(...tail calls...)
Player:128: in method 'moveBy'
Player:121: in method 'keyPress'
GameRunner:252: in method 'keyPress'
Main:34: in function 'keyboard'
OK, now that I’ve done this clever thing, how do I implement setMonsterTurn
?
function GameRunner:stopFloater()
self:setPlayerTurn()
end
function GameRunner:startFloater()
self:setMonsterTurn()
end
I think we really want the start to set monster turn, but the stop to just do turnComplete
:
function GameRunner:stopFloater()
self:turnComplete()
end
function GameRunner:startFloater()
self:setMonsterTurn()
end
To set monster turn, I think I’ll ask the turns:
function GameRunner:setMonsterTurn()
self.turn = self.turn:getMonsterTurn()
end
function MonsterTurn:getMonsterTurn()
return self
end
function PlayerTurn:getMonsterTurn()
return self.next
end
This seems good, let’s see what happens now. Well, the game plays as usual … but I can still move the princess while the scroll is running. That seems off.
function Player:keyPress(key)
if not self:itsOurTurn() then return false end
local step = PlayerSteps[key]
if step then
self:moveBy(step)
end
self:turnComplete()
end
function Player:itsOurTurn()
return self.runner:itsPlayerTurn()
end
function GameRunner:itsPlayerTurn()
return self.turn:isPlayerTurn()
end
function MonsterTurn:isPlayerTurn()
return false
end
function PlayerTurn:isPlayerTurn()
return true
end
That sure looks to me like it ought to work, unless someone is moving who shouldn’t. I think this is another timing problem, same as the other one, because we need a sort of stack of movement inhibition. My scheme of just jamming a monster turn into the turn variable doesn’t hold water.
Curiously, I was really tempted to stop after the most recent commit and call it a day. But it seemed possible to fix that one little thing. I think I was mistaken.
Now I’m torn. I think this setup is better, and it actually functions no worse than before. Let’s verify that this is the same nesting problem and then decide to commit or revert.
I’ll instrument the setters and next stuff. Some testing convinces me that what’s happening is that when we move the monsters, in Monster:execute
, they might trigger an attack, and if they do, the encounter will call startFloater
and that will (redundantly) set the turn to monster, and then the running Monster:execute
will do turnComplete
, freeing the princess to move.
We have to face reality here. We have a stack kind of situation. Nesting, if you will. Call it what we might, implement it as we might, we have a situation where clearing the frozen status of the player is done too soon. We can fix that by counting or other means, as discussed yesterday.
Facing Reality
I think reality is staring me in the face here. The scheme of going to a Turn object hasn’t helped. It seemed like a good idea, and even now I feel that it has some merit, but in fact, it hasn’t addressed the fundamental issue of getting the game to behave in a turn-based fashion, where monsters and players take turns, and where there is at least a bit of control over when they switch.
Here’s another way of thinking of the problem:
We’re at some point in the game, and we want to ignore user input until some event occurs. Presently we have two events: the monsters have had their turn, and a crawl has started that should run to completion. We foresee another situation, something like “combat has started, let it run to completion”. That might use the crawl lock, or it might not.
Our use of the Turns is not symmetric: the monsters just execute and release (which is part of the problem), and when the PlayerTurn is in effect, that just turns on attention to the buttons and keyboard and after a key or button is pressed, the player turn is released. (This may not be correct in the longer term. Some presses may not release immediately. I’m kind of thinking that will be dealt with at the lower level.
It can happen that some bit of the system is running the monsters and knows its done, and it says turnComplete
. While the monsters are running, another part (the Crawl) wants control to reside in the monsters (or, more to the point, wants control not to be given to the player, until it (the Crawl) knows that it is done. Then it will say “turnComplete”, via its callback to GameRunner.
This second desire not to run the player needs to stack on top of the first, so that when the first one is done, it sort of pops the stack but there’s still a “no players” item on the stack. This makes me think of staking the events. But it’s even worse than that.
The second “push”, the Crawl, starts after the first push. But it ends after the first one is complete. It’s not really a stack. The logic isn’t nested.
We might get away with some kind of stack or counter, but the real truth is that we have a situation like this:
[start {start ]stop }stop
It works a bit like read locks on a file. They can start and end in any order. They are kind of the same, and often we can just tally them up and down and we’re fine. Other times, we might have to keep track of the overlapping areas reserved. Fortunately for us, we probably don’t have to do that.
Unfortunately for us, our objects aren’t really helping us at all with our problem. They seemed like a good idea, but in fact, they’re not bearing their weight at all.
Backing Our Mind Out of the Hole
Let’s back up a bit. We have one item that really matters, this one:
function Player:keyPress(key)
if not self:itsOurTurn() then return false end
local step = PlayerSteps[key]
if step then
self:moveBy(step)
end
self:turnComplete()
end
function Player:itsOurTurn()
return self.runner:itsPlayerTurn()
end
function Button:touched(aTouch, player)
if aTouch.state ~= BEGAN then return end
if not player:itsOurTurn() then return end
local x = aTouch.pos.x
local y = aTouch.pos.y
local ww = self.w/2
local hh = self.h/2
if x > self.x - ww and x < self.x + ww and
y > self.y - hh and y < self.y + hh then
player[self.name](player)
end
player:turnComplete()
end
The whole point of all this comes down to properly implementing GameRunner:itsPlayersTurn
.
Right now, that’s this:
function GameRunner:itsPlayerTurn()
return self.turn:isPlayerTurn()
end
Which checks the turn instance, blah blah.
There must be a simpler way. But now we have all these turn objects in here and such. Do we want a stack of turn objects that we’re allowed to use? Or a simple counter?
What do we want to have happen, at base?
The normal mode should be that the player can enter commands. At some intervals, such as the end of a command (perhaps more than one touch), the monsters get a turn. Perhaps there’s a timer event that lets them move while the princess dithers.
There are some long-running activities. When these are running, the princess is not allowed to move. When they’re not, she is allowed to move, but it has to be her turn.
Can it be that it is always her turn, except during a long-running activity?
Honestly, I don’t know. I wish a few of you were here to kick this around with me.
But clearly we have too much mechanism going on here, so let’s simplify things and remove the excess fat.
Start From the Blocking Floater
We’ll start from the BlockingFloater and change the meaning of its messages to GameRunner to mean “princess cannot go”.
function GameRunner:stopFloater()
self.playerCanRun = true
end
function GameRunner:startFloater()
self.playerCanRun = false
end
Now we’ll change the accessor for that:
function GameRunner:itsPlayerTurn()
return self.playerCanRun
end
Now I think I want to rip out the Turn stuff right now, so that things will explode and we can get back to something sensible.
function GameRunner:createTurns()
local pt = PlayerTurn(self)
local mt = MonsterTurn(self)
print(mt)
mt:setNext(pt)
pt:setNext(mt)
self.turn = mt
end
Deleted. Remove call to it from GameRunner:init
.
function GameRunner:createLevel(count)
...
self:runCrawl(self.initialCrawl)
self.playerCanMove = true
end
What happens if we run this? A glance at Player suggests:
function Player:keyPress(key)
if not self:itsOurTurn() then return false end
local step = PlayerSteps[key]
if step then
self:moveBy(step)
end
self:turnComplete()
end
function Player:turnComplete()
self.runner:turnComplete()
end
function GameRunner:turnComplete()
self.turn = self.turn:getNext()
self.turn:execute(self)
end
function MonsterTurn:execute(runner)
runner:moveMonsters()
runner:turnComplete()
end
Let’s just move the monsters and turn the player flag off and on instead:
function GameRunner:turnComplete()
self.playerCanMove = false
self:moveMonsters()
self.playerCanMove = true
end
I suspect there are a few calls that will break, but this should nearly work. Let’s try it. The princess cannot move. Why not?
function GameRunner:itsPlayerTurn()
return self.playerCanRun
end
Perhaps not referencing both playerCanMove
and playerCanRun
would be better. Which is it? playerCanMove
.
function GameRunner:itsPlayerTurn()
return self.playerCanMove
end
The game works as before. Are you serious? All this work and a simple flag is still good enough? Let’s remove all the turn stuff. No, first commit: using simple flag to stop player movement.
Now remove the Turns etc. Game still plays. Are there references? I’ll do some searches just in case. There was a residual method GameRunner:setMonsterTurn
, which I removed.
Commit: remove Turn objects
Now let’s sum up before we have to delete more useless code.
Summary
Well. The system is just as turn-based as it ever has been, at the hands of a simple flag rather than the elegant but ultimately useless Turn objects. It’s not perfectly turn-based, because the Floater still doesn’t wind up blocking the player’s moves. The fault, however, is with GameRunner. It’s sent the start and stop messages from the Floater, but it just sets the flag rather than ticking it up and down.
I believe that just ticking it up and down will result in what I want, but it’s 1250 and I’m well into overtime.
What have we learned today, class?
Well, we do have a bit of improvement between Floater and GameRunner, in the form of the listener approach and the Null Object listener that Floater provides for itself if no one else wants to listen. So that connection is as good as it ever was, and is far nicer code.
As I mentioned above, I would probably do well to use the Null Object pattern more often: It would likely help me get rid of some nil checks. It’s a simple pattern if one is familiar with it. I am in fact familiar with it, and you, well, if you happened not to be, now you know. Give it a try.
But the Turn object, which seemed likely to be what I needed, turned out not to solve the problem it was set to solve, the nesting of decisions about whether the player can go1.
So, once reality smashed into my forehead enough times, I dropped back to a simple flag and removed the Turns. This will at least leave us a clear field for next time.
The lessons here are at least two:
First, I was going to do a new Combat thing and let that drive the need for stopping and starting. I deviated from that plan, and perhaps that was not wise. That said, the current floater has the need for stopping and starting, so it might be OK anyway.
Second, when we built something that isn’t suitable, let’s remove it as soon as we possibly can. Let’s not live with our old mistakes, let’s make fresh clean new ones.
I hope to see you then.
-
Does she go? Is she a goer? ↩