Dungeon 74
Just some little things. There may be nothing interesting to see here. One never knows, though, do one?”
Pauses During Crawl
Whenever the crawl is going, player and monster activity stop. In encounters, with their scripted behavior, that’s needed. But I think I’d like to use it for occasions where pausing isn’t necessary. Even in the opening crawl, we could allow the player to start exploring.
OK, how does this thing work? We create a Floater (is that a bad name? Probably. Never mind.):
function Floater:init(runner, provider, yOffsetStart, lineSize, lineCount)
self.runner = runner
self.provider = coroutine.create(provider)
self.yOffsetStart = yOffsetStart
self.lineSize = lineSize
self.lineCount = lineCount
self:startCrawl()
end
function Floater:startCrawl()
self.yOff = self.yOffsetStart
self.buffer = {}
if self.runner then self.runner:stopMonsters() end
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
if self.runner then self.runner:startMonsters() end
end
end
There’s more to it, but that’s where the stop/start action is. Note that we don’t start again until the crawl is empty. So we can’t just conveniently wrap it all up, since the crawl runs asynchronously with everything else.
But we can easily add a defaulted parameter. We’ll default to not allowing movement, though either way may turn out to be more common.
function Floater:init(runner, provider, yOffsetStart, lineSize, lineCount, allowMovement)
self.allowMovement = allowMovement or false
self.runner = runner
self.provider = coroutine.create(provider)
self.yOffsetStart = yOffsetStart
self.lineSize = lineSize
self.lineCount = lineCount
self:startCrawl()
end
Then we can plug in use of the flag:
function Floater:startCrawl()
self.yOff = self.yOffsetStart
self.buffer = {}
if not self.allowMovement and self.runner then self.runner:stopMonsters() end
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
if not self.allowMovement and self.runner then self.runner:startMonsters() end
end
end
I’m not loving that not
but it seems a better default.
Let’s test now. Everything should be the same, no movement during a crawl. Right. I notice again that it’s faster now, remind me to slow it a bit.
No. I am not a bright man. We don’t need to pass this field in init. We need to pass it in runCrawl, although we are passing in a null message on init.
function Floater:runCrawl(aFunction)
if coroutine.status(self.provider) == "dead" then
self.provider = coroutine.create(aFunction)
self:startCrawl()
else
print("crawl ignored while one is running")
end
end
Let’s give this guy our flag:
function Floater:runCrawl(aFunction, allowMovement)
if coroutine.status(self.provider) == "dead" then
self.allowMovement = allowMovement or false
self.provider = coroutine.create(aFunction)
self:startCrawl()
else
print("crawl ignored while one is running")
end
end
Now to use it in the initial crawl:
function GameRunner:createLevel(count)
self:createRandomRooms(count)
self:connectRooms()
self:convertEdgesToWalls()
local r1 = self.rooms[1]
local rcx,rcy = r1:center()
local tile = self:getTile(vec2(rcx,rcy))
self.player = Player(tile,self)
self.monsters = self:createThings(Monster,9)
for i,monster in ipairs(self.monsters) do
monster:startAllTimers()
end
self.keys = self:createThings(Key,5)
self:createThings(Chest,5)
self.buttons = {}
table.insert(self.buttons, Button("left",100,200, 64,64, asset.builtin.UI.Blue_Slider_Left))
table.insert(self.buttons, Button("up",200,250, 64,64, asset.builtin.UI.Blue_Slider_Up))
table.insert(self.buttons, Button("right",300,200, 64,64, asset.builtin.UI.Blue_Slider_Right))
table.insert(self.buttons, Button("down",200,150, 64,64, asset.builtin.UI.Blue_Slider_Down))
self:runCrawl(self.initialCrawl, true)
end
Now I expect to be able to move during the initial crawl. However I cannot. That’s discouraging. Better revert.
Why am I reverting rather than just debugging this? Well, because I’ve clearly missed some key fact and I don’t know what it is, plus I put a parameter where it shouldn’t be, plus I don’t like those not
s anyway.
Let’s do again.
If we think more deeply about what we want with the crawl, it seems that we want something to happen just before the crawl starts, and something else to happen after it ends. The only case we have now is a call to stopMonsters
and startMonsters
. Should we provide for other possibilities? And if so, should we do it now? We don’t have another possibility in mind, so my default answer is No, this kind of generalization isn’t appropriate.
Stick with the simple flag:
function Floater:runCrawl(aFunction)
if coroutine.status(self.provider) == "dead" then
self.provider = coroutine.create(aFunction)
self:startCrawl()
else
print("crawl ignored while one is running")
end
end
This seems to be the place to put it, unless we were to make two different entry points. I think the flag will suffice.
function Floater:runCrawl(aFunction, stopAction)
if coroutine.status(self.provider) == "dead" then
self.provider = coroutine.create(aFunction)
self:startCrawl(stopAction)
else
print("crawl ignored while one is running")
end
end
This time we push down the flag as far as it can go.
function Floater:startCrawl()
self.yOff = self.yOffsetStart
self.buffer = {}
if self.runner then self.runner:stopMonsters() end
self:fetchMessage()
end
That becomes:
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
And we have to start them …
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
And now we’d best check our calls to runCrawl and startCrawl.
function GameRunner:runCrawl(aFunction, stopAction)
self.cofloater:runCrawl(aFunction, stopAction)
end
function GameRunner:createLevel(count)
self:createRandomRooms(count)
self:connectRooms()
self:convertEdgesToWalls()
local r1 = self.rooms[1]
local rcx,rcy = r1:center()
local tile = self:getTile(vec2(rcx,rcy))
self.player = Player(tile,self)
self.monsters = self:createThings(Monster,9)
for i,monster in ipairs(self.monsters) do
monster:startAllTimers()
end
self.keys = self:createThings(Key,5)
self:createThings(Chest,5)
self.buttons = {}
table.insert(self.buttons, Button("left",100,200, 64,64, asset.builtin.UI.Blue_Slider_Left))
table.insert(self.buttons, Button("up",200,250, 64,64, asset.builtin.UI.Blue_Slider_Up))
table.insert(self.buttons, Button("right",300,200, 64,64, asset.builtin.UI.Blue_Slider_Right))
table.insert(self.buttons, Button("down",200,150, 64,64, asset.builtin.UI.Blue_Slider_Down))
self:runCrawl(self.initialCrawl, false)
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:startActionWithMonster(aMonster)
if aMonster:isDead() then return end
--Encounter(self,aMonster):attack()
self.runner:runCrawl(createEncounter(self,aMonster), true)
end
Again, I expect to be able to move during the initial crawl. And I’m afraid that I won’t be able to.
It’s worse than last time. Now I can’t move even after the crawl stops. WTH?
What have I missed? I’m going to resort to some tracing.
function Floater:startCrawl(stopAction)
self.stopAction = stopAction
print("startCrawl ", self.stopAction)
self.yOff = self.yOffsetStart
self.buffer = {}
if self.stopAction and self.runner then print("stopping"); self.runner:stopMonsters() end
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
print("increment ", self.stopAction)
if self.stopAction and self.runner then print("starting"); self.runner:startMonsters() end
end
end
Here’s what prints:
startCrawl true
stopping
startCrawl false
increment false
It appears that the initial startCrawl is stopping action but not ending it. That will be the empty message from the initial creation of the Floater. What if we call that with false?
function Floater:init(runner, provider, yOffsetStart, lineSize, lineCount)
self.runner = runner
self.provider = coroutine.create(provider)
self.yOffsetStart = yOffsetStart
self.lineSize = lineSize
self.lineCount = lineCount
self:startCrawl(false)
end
(Possibly we shouldn’t start a null crawl at all. Let’s see what this does.)
OK, now it works as anticipated, but I wonder why the use of true
didn’t stop and start immediately. Something about an empty set of messages, I expect. Let’s just not do that and see if things work as intended.
function Floater:init(runner, provider, yOffsetStart, lineSize, lineCount)
self.runner = runner
--self.provider = coroutine.create(provider)
self.yOffsetStart = yOffsetStart
self.lineSize = lineSize
self.lineCount = lineCount
--self:startCrawl(false)
end
It turns out that we created a provider with the intention of making this work:
function Floater:runCrawl(aFunction, stopAction)
if coroutine.status(self.provider) == "dead" then
self.provider = coroutine.create(aFunction)
self:startCrawl(stopAction)
else
print("crawl ignored while one is running")
end
end
So we “needed” a dead coroutine to get things going. My first fix is this:
function Floater:runCrawl(aFunction, stopAction)
if self.provider==nil or coroutine.status(self.provider) == "dead" then
self.provider = coroutine.create(aFunction)
self:startCrawl(stopAction)
else
print("crawl ignored while one is running")
end
end
And that works. Better, however, is this:
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:okToProceed()
return self.provider==nil or coroutine.status(self.provider) == "dead"
end
Still works fine. Remove the commented-out lines and the prints.
function Floater:init(runner, yOffsetStart, lineSize, lineCount)
self.runner = runner
self.provider = nil
self.yOffsetStart = yOffsetStart
self.lineSize = lineSize
self.lineCount = lineCount
end
And the creator:
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)
end
There are tests as well. Let’s see what happens. Game works, tests need revision. They both just need this at the beginning, to provide data to test:
_:test("floater initialize", function()
local fl = Floater(nil, 50, 25, 4)
fl:runCrawl(msg)
...
_:test("floater pulls messages appropriately", function()
local fl = Floater(nil,50,25,4)
fl:runCrawl(msg)
Tests are green. Commit: crawl now allows optional motion. Initial crawl allows motion.
That took longer than I had expected. Not too surprising, since the whole coroutine thing was only theory to me until I did this one, so one would expect the implementation to be ragged. I think it’s a bit better now.
More?
What else? I did have something in mind. Oh, yes, health.
Some of the monsters are pretty wicked, and I think when we find a health thing, we should get more points. Let’s change it to roll a random addition.
function Health:init(aTile,runner, points)
self.tile = aTile
self.runner = runner
self.points = points or 1
end
function Health:draw(tiny)
if tiny then return end
pushStyle()
spriteMode(CENTER)
local g = self.tile:graphicCenter()
sprite(asset.builtin.Planet_Cute.Heart,g.x,g.y+10, 35,60)
popStyle()
end
function Health:giveHealthPoints()
self.tile:removeContents(self)
return self.points
end
We could just initialize them, but let’s make it actually random unless the value is provided. Health items are only found in Chests at present:
function Chest:open()
if self:isOpen() then return end
self.pic = self.openPic
self.tile:addDeferredContents(Health(self.tile, self.runner))
end
We’re not using the points value. I should have never implemented it: it just confused me. Speculation strikes again.
function Health:giveHealthPoints()
self.tile:removeContents(self)
return self:randomPoints()
end
function Health:randomPoints()
return math.random(4,10)
end
I’ll give that a run. Immediately, I roll enough health to overflow the bar graph. We need it to max at 20.
function Player:startActionWithHealth(aHealth)
self.healthPoints = self.healthPoints + aHealth:giveHealthPoints()
end
Tch tch.
function Player:startActionWithHealth(aHealth)
self:addHealthPoints(aHealth:giveHealthPoints())
end
function Player:addHealthPoints(points)
self.healthPoints = math.min(20, self.healthPoints + points)
end
Seems to work as anticipated. I even managed to vanquish a serpent by fighting near a chest and running to it when I got weak.
Commit: health object provides random health points 4-9.
Time for One More?
I’d like for the added health value to create a floater, saying something like “+5 health!!” when we use it. That can be done in two ways.
Recall that Floater expects to receive a function that it puts into a coroutine, to return the messages it wants.
For example, the initial crawl:
function GameRunner:initialCrawl()
coroutine.yield("Welcome to the Dungeon.")
coroutine.yield("Here you will find great adventure,")
coroutine.yield("fearsome monsters, and fantastic rewards.")
coroutine.yield("Some of you may die, but I am willing")
coroutine.yield("to make that sacrifice.")
end
We run the crawl like this:
...
self:runCrawl(self.initialCrawl, false)
...
So the direct way to do this is this:
function Player:addHealthPoints(points)
self.runner:runCrawl(healthFloater, false)
self.healthPoints = math.min(20, self.healthPoints + points)
end
function healthFloater(message)
coroutine.yield(message)
end
Well, then again, maybe it isn’t. Because the message doesn’t come out. Instead I get the message “crawl ignored while one is running” when I get into a battle after grabbing a health.
So that function must not be quite the thing.
Sheesh, of course. It doesn’t have a message, and it kind of can’t, at least not like that. We have to be much more crafty. This is likely to get nasty, and it’s nearly time for supper.
Ah. I guessed right the first time:
function Player:addHealthPoints(points)
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
This works just as intended.
This’ll be a good place to close.
Summing Up
I mentioned two ways to do the floater. The more general thing would be to have a generic way to create a text message, or a collection of them, and generate a crawl from them. The next time we need to do a simple crawl, we’ll figure that out. For now, we’re good.
Overall, this was a bit fast and loose, but things went OK. I was doing some fairly tricky things, between the asynchronous stop/start logic and the coroutine for the crawl, and all that. The health addition was interesting. It could surely have been done with TDD, and if I had, I’d probably have thought to test a total bigger than 20. But I got lucky and saw the problem happen right away.
It’s tricky, when doing things without TDD, to suddenly drop into TDD when the situation better supports it.
And maybe I coulda shoulda TDD’d the other things.
I don’t know. I”m just a programmer, wandering in the wilderness, trying to figure out what to do and how to improve.
Join me next time!
–