More on Encounter coroutine version. Rationalizing not doing much more TDD. Will he be sorry?

Since I’m just transliterating the old version of the Encounter, I plan to do less TDD on it. I may regret this, especially since the old tests and class will be removed at the end of this exercise. In addition, I’m sure I’ve already made one mistake.

Nonetheless, though I would not recommend you do this at home, I’m planning to work without a net, at least until I fall and break my neck.

Mistake you said? Yes, I think so. Remember that I moved a line from attack down to firstAttack:

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

function firstAttack(attacker,defender)
    yield(attacker:name().." is faster!")
    yield(attacker:name().." strikes!")
end

That “is faster” line was duplicated up inside the if blocks in attack. I saw that by pushing it down, I could save the duplication. However, I had forgotten this, from the object-oriented firstAttack:

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

The firstAttack function calls itself on a riposte. This means that in the new form, it will then say “X is faster” for whoever is riposting. This isn’t what we want. So, let’s put it back to right.

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

function firstAttack(attacker,defender)
    yield(attacker:name().." strikes!")
end

This may seem like an odd time to stop testing, given that I just implemented a defect. But I’m only human, and I’m impatient to get this going. We’ll see how it goes, We’re all good programmers here, and we have more than one way to avoid mistakes. Or, well, anyway we’re all pretty good programmers here …

I’m going to move more logic in from the Encounter object. All of it in one go, I think:

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

This requires me to bring in attackMisses and attackStrike, converting:

function Encounter:attackMisses(attacker, defender)
    self:log(defender:name().." avoids strike!")
end

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

To this:

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

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

With this in place, I think the conversion to coroutine is done. It should be possible to use this function now inside the floating message logic.

There’s a problem, which is that the original opening crawl is a simple list of lines. We could convert that to a coroutine-driven list pretty easily, and in the longer term we probably will. But right now, I’m more interested in getting this encounter logic plugged in and then deleting the old stuff.

However, as I sit here trying to rationalize jamming this code in, it occurs to me that if the opening crawl was a coroutine list, it would be much easier to test whether it’s working.

Anyway, first we need to review how the list currently works. It’s really rather complicated. Let’s look at the whole FML object:

-- FloatingMessageList
-- RJ 20200106

FloatingMessageList = class()

function FloatingMessageList:init(runner)
    self.runner = runner
    self.messages = {}
    self.yOffStart = 50
    self.lineSize = 25
end

function FloatingMessageList:addMessages(messages)
    self:stopMonsters()
    if (#self.messages == 0) then
        self:initForMoving()
    end
    table.move(messages,1,#messages, #self.messages+1, self.messages)
end

function FloatingMessageList:draw()
    if #self.messages == 0 then return end
    pushMatrix()
    pushStyle()
    fill(255)
    fontSize(20)
    local pos = self.runner:playerGraphicCenter()
    pos = pos + vec2(0,self.yOff)
    for i = 1,self:numberToDisplay() do
        text(self:getText(i), pos.x, pos.y)
        pos = pos - vec2(0,self.lineSize)
    end
    popStyle()
    popMatrix()
    self:increment(1)
end

function FloatingMessageList:getText(anInteger)
    local msg = self.messages[anInteger]
    if type(msg) == "string" then
        return msg
    else
        return msg:text()
    end
end

function FloatingMessageList:increment(n)
    self.yOff = self.yOff + (n or 1)
    if self:rawNumberToDisplay() > 4 then
        self.yOff = self.yOff - self.lineSize
        table.remove(self.messages,1)
        if #self.messages == 0 then
            self:startMonsters()
        end
    end
end

function FloatingMessageList:initForMoving()
    self.yOff = self.yOffStart
end

function FloatingMessageList:numberToDisplay()
    return math.min(self:rawNumberToDisplay(),#self.messages)
end

function FloatingMessageList:rawNumberToDisplay()
    return 1 + (self.yOff - self.yOffStart) // self.lineSize
end

function FloatingMessageList:startMonsters()
    if self.runner then self.runner:startMonsters() end
end

function FloatingMessageList:stopMonsters()
    if self.runner then self.runner:stopMonsters() end
end

function FloatingMessageList:yOffset()
    return self.yOff
end

Why the getText function? Aren’t all these messages just strings? I believe they are. I think this “feature” is intended to deal with a future where the message array isn’t just a string, but a more complex object that includes operations to perform along the way. That was my original plan, based on Bryan’s idea. This appears to be a bit premature.

Hm. My original plan was to adapt this code to the new coroutine style. I’m toying with the idea of starting with a new object that’s custom made for the purpose. The current implementation is crafted to deal with the possibility that a new list of messages will arrive while we’re still displaying an existing list. It appends the new lines to the bottom of the list. There are two reasons why we don’t want to do that henceforth.

First, the FML now calls stopMonsters, which basically stops anything from happening while the crawl runs. We may change highlighting and move things around inside our coroutine, but no new messages are going to be produced.

Second, the new scheme will be handed a function to call to get new lines. That function will run out of lines at some point. Then, later, we could be called with a new function. If someone did call us with a new function while we’re running, bad things could happen.

But perhaps not. Perhaps we’ll save up a list of functions instead of a list of lines, and keep running until all the functions are used up. We could make that work.

Nonetheless, I am inclined to create a new floating message display object. It kind of flies in the face of my preferred approach, which is to evolve the code, but here I think we should recognize that since we have a new kind of message producer, it may make sense to have a new kind of consumer as well.

However (this thought tree is getting deeper) the floating message display shows the same messages many times, scrolling them up a bit at a time. It can’t call back to the coroutine for another copy. It’s going to have to buffer them.

Let’s build up a few new objects, leaving FML in place for now, with intention to remove it. We may have a few more days’ work to do before we can install our new coroutine. We’ll see.

Let’s envision a Crawl object, that has an array of up to N lines of text, and a y-region of y coordinates within which to display that array, scrolling upward. Let’s give it a function to call when it wants a new line, and assume that the function returns a blank line when there are no more lines to give. (We’ll have to improve our current encounter coroutine to do that.)

When the top line of our N lines gets to the top of the y-region, our Crawl will delete the first element of its array, and will request a new last element. The text display origin will drop down one line when this happens, so that the formerly second line will not just leap to the top.

I think I’ll draw us a picture.

diagram

We have some array of lines. I’m supposing it’s from 1-4 lines long but it may not matter. When we start scrolling up, we set our display offset, yOff to yOffsetStart. Starting there, we display lines downward in Y until we would draw below the bottom of the region, then don’t draw any more lines.

As time passes, we increment yOffset upward. After a while, we can display 2 or 3 or even 4 lines. When, finally, the fourth line start (yOff) increments above the top of our region, we delete the first line from our table, and reduce yOff by one line height (which will cause the formerly second now first line to display exactly where it should). And we’ll try to get another line into our array, calling our line-providing coroutine.

I Give Up

Even though I have essentially this logic running in the FML object, I grant that this logic is tricky enough that I’ll be wise to TDD it. The only thing holding me back is that CodeaUnit isn’t very helpful about letting me turn off whole racks of tests. Nonetheless this needs TDD.

I’ll commit what I have, which is working. That will give me a save point if I need one.

First, I’ll need a simple coroutine to provide messages. Let’s have it provide five, with an expectation that we can only display four lines at a time.

I’ll create a new test tab, TestFloater.

-- TestFloater
-- RJ 20200112

function testFloater()
    CodeaUnit.detailed = true
    
    _:describe("Floater", function()
        
        _:before(function()
        end)
        
        _:after(function()
        end)
        
        _:test("Hookup", function()
            _:expect("hook").is("hookup")
        end)
        
    end)
    
end

Test fails as expected. All other tests commented out. Now for a little coroutine thingie.

        _:test("messages are OK", function()
            local msg = coroutine.wrap(messages)
            _:expect(msg()).is("Message 1")
        end)

function messages()
    for i = 1,5 do
        coroutine.yield("Message "..i)
    end
    coroutine.yield("")
end

Extend that test just for drill:

        _:test("messages are OK", function()
            local msg = coroutine.wrap(messages)
            _:expect(msg()).is("Message 1")
            msg(); msg(); msg()
            _:expect(msg()).is("Message 5")
            _:expect(msg()).is("")
            _:expect(msg()).is(nil)
        end)

Works. Now for the real work.

        _:test("floater initialize", function()
            local fl = Floater(msg, 50, 25, 4)
            _:expect(fl.yOffsetStart).is(50)
            _:expect(fl.lineSize).is(25)
            _:expect(fl.lineCount).is(4)
        end)

First, the initial values.

~~~luaFloater = class()

function Floater:init(provider, yOffsetStart, lineSize, lineCount) self.provider = provider self.yOffsetStart = yOffsetStart self.lineSize = lineSize self.lineCount = lineCount end


So far so good. Now at this point, the number of lines we can display should be 1. Let's ask:

~~~lua
        _:test("floater initialize", function()
            local fl = Floater(msg, 50, 25, 4)
            _:expect(fl.yOffsetStart).is(50)
            _:expect(fl.lineSize).is(25)
            _:expect(fl.lineCount).is(4)
            _:expect(fl.linesToDisplay()).is(1)
        end)

Fake it till you make it:

function Floater:linesToDisplay()
    return 1
end

Tests still green. Now where are you going to display that? We haven’t set up y offset yet.

        _:test("floater initialize", function()
            local fl = Floater(msg, 50, 25, 4)
            _:expect(fl.yOffsetStart).is(50)
            _:expect(fl.lineSize).is(25)
            _:expect(fl.lineCount).is(4)
            _:expect(fl.linesToDisplay()).is(1)
            _:expect(fl.yOffset()).is(50)
        end)

Fake that too:

function Floater:yOffset()
    return 50
end

Increment by 25 lines (one line size):

        _:test("floater initialize", function()
            local fl = Floater(msg, 50, 25, 4)
            _:expect(fl.yOffsetStart).is(50)
            _:expect(fl.lineSize).is(25)
            _:expect(fl.lineCount).is(4)
            _:expect(fl.linesToDisplay()).is(1)
            _:expect(fl.yOffset()).is(50)
            fl:increment(25)
            _:expect(fl:yOffset()).is(75)
            _:expect(fl:linesToDisplay()).is(2)
        end)

Those last two fail. First, define increment:

function Floater:increment(n)
    
end

Now we fail as intended:

2: floater initialize  -- Actual: 50, Expected: 75

We need to set up a y offset, increment it, return it.

I’ve made my standard error number 77, dots instead of colons: Test is:

        _:test("floater initialize", function()
            local fl = Floater(msg, 50, 25, 4)
            _:expect(fl.yOffsetStart).is(50)
            _:expect(fl.lineSize).is(25)
            _:expect(fl.lineCount).is(4)
            _:expect(fl:linesToDisplay()).is(1)
            _:expect(fl:yOffset()).is(50)
            fl:increment(25)
            _:expect(fl:yOffset()).is(75)
            _:expect(fl:linesToDisplay()).is(2)
        end)

Code is:

function Floater:init(provider, yOffsetStart, lineSize, lineCount)
    self.provider = provider
    self.yOffsetStart = yOffsetStart
    self.lineSize = lineSize
    self.lineCount = lineCount
    self.yOff = self.yOffsetStart
end

function Floater:increment(n)
    self.yOff = self.yOff + (n or 1)
end

function Floater:linesToDisplay()
    return 1
end

function Floater:yOffset()
    return self.yOff
end

Error as expected now is:

2: floater initialize  -- Actual: 1, Expected: 2

We want to compute how many lines we have room for between yOffsetStart and yOffset. That is:

function Floater:linesToDisplay()
    return 1 + (self.yOff - self.yOffsetStart)//self.lineSize
end

OK, our Floater knows how many lines to display given where yOff is. Now what?

We “know” that we want Floater to maintain a buffer of (up to) lineCount messages. Should we attempt to fill it immediately? No! The whole point of our coroutine delivery of messages is to pull them only when we are willing to evoke the next bits of action on the part of the attacker and defender. So even for the first few messages, we want to pull … only when the number we need is greater than the number we have.

I think we’ve done enough in this test. Let’s create a new one that tells the story.

We’ll start with this:

        _:test("floater pulls messages appropriately", function()
            local fl = Floater(msg,50,25,4)
            _:expect(#fl.buffer).is(1)
        end)

As soon as Floater inits, it should pull one message. We’ll invade it to count the buffer size. So in init:

function Floater:init(provider, yOffsetStart, lineSize, lineCount)
    self.provider = provider
    self.yOffsetStart = yOffsetStart
    self.lineSize = lineSize
    self.lineCount = lineCount
    self.yOff = self.yOffsetStart
    self.buffer = {}
end

Test should fail finding zero expecting one.

3: floater pulls messages appropriately  -- Actual: 0, Expected: 1

So, I’m imagining that it’s OK to pull a message right in init.

function Floater:init(provider, yOffsetStart, lineSize, lineCount)
    self.provider = provider
    self.yOffsetStart = yOffsetStart
    self.lineSize = lineSize
    self.lineCount = lineCount
    self.yOff = self.yOffsetStart
    self.buffer = {}
    self:fetchMessage()
end

function Floater:fetchMessage()
    table.insert(self.buffer, self.msg())
end

I expect this to run green. Silly me:

3: floater pulls messages appropriately -- Floater:14: attempt to call a nil value (field 'msg')

Might make more sense to call provider.

function Floater:fetchMessage()
    table.insert(self.buffer, self.provider())
end

Tests are green. Check the message:

        _:test("floater pulls messages appropriately", function()
            local fl = Floater(msg,50,25,4)
            _:expect(#fl.buffer).is(1)
            _:expect(fl.buffer[1]).is("Message 1")
        end)

Test still green.

Increment 24, expect the same, then increment once and expect a new message:

        _:test("floater pulls messages appropriately", function()
            local fl = Floater(msg,50,25,4)
            _:expect(#fl.buffer).is(1)
            _:expect(fl.buffer[1]).is("Message 1")
            fl:increment(24)
            _:expect(#fl.buffer).is(1)
            _:expect(fl.buffer[1]).is("Message 1")
            fl:increment()
            _:expect(#fl.buffer).is(2)
            _:expect(fl.buffer[2]).is("Message 2")
        end)

I expect to fail looking for 2 and finding 1.

3: floater pulls messages appropriately  -- Actual: 1, Expected: 2

So in increment:

function Floater:increment(n)
    self.yOff = self.yOff + (n or 1)
    if #self.buffer < self:linesToDisplay() then
        self:fetchMessage()
    end
end

I rather expect this to work. Perhaps mysteriously, it does.

Now we come to a tricky bit. I think it’s clear that this will keep fetching as needed. Now we need to ensure that we won’t go beyond 4 lines in the buffer. First I’ll make sure they all come in:

        _:test("floater pulls messages appropriately", function()
            local fl = Floater(msg,50,25,4)
            _:expect(#fl.buffer).is(1)
            _:expect(fl.buffer[1]).is("Message 1")
            fl:increment(24)
            _:expect(#fl.buffer).is(1)
            _:expect(fl.buffer[1]).is("Message 1")
            fl:increment()
            _:expect(#fl.buffer).is(2)
            _:expect(fl.buffer[2]).is("Message 2")
            fl:increment(25)
            _:expect(fl.buffer[3]).is("Message 3")
            fl:increment(25)
            _:expect(fl.buffer[4]).is("Message 4")
        end)

This should run correctly. Better yet, it actually does.

Now when we increment by another 25, we should delete message 1, and fetch a new one, leaving messages 2-5 in buffer[1-4]:

            fl:increment(25)
            _:expect(#fl.buffer).is(4)
            _:expect(fl.buffer[1]).is("Message 2")
            _:expect(fl.buffer[4]).is("Message 5")

This will fail with 5 instead of 4, etc:

3: floater pulls messages appropriately  -- Actual: 5, Expected: 4
3: floater pulls messages appropriately  -- Actual: Message 1, Expected: Message 2
3: floater pulls messages appropriately  -- Actual: Message 4, Expected: Message 5

So we need to detect the overflow and delete one message. In addition, we need to step yOff down, though I’ve not tested that. I’m going to do it while it’s on my mind.

function Floater:increment(n)
    self.yOff = self.yOff + (n or 1)
    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
end

I expect the test to be green. And it is. For documentation purposes, I should test the yOffset.

            _:expect(fl:yOffset()).is(125)
            fl:increment(25)
            _:expect(fl:yOffset()).is(125)
            _:expect(#fl.buffer).is(4)
            _:expect(fl.buffer[1]).is("Message 2")
            _:expect(fl.buffer[4]).is("Message 5")

And the test is still green.

We’re still not done. When we scroll message 2 off the top, we will find no more messages to display. So we’ll expect the number in the buffer to decline down to zero.

This test is getting to be rather long. But it’s telling a long story, so rather than do another test, I’ll extend this one.

            fl:increment(25)
            _:expect(fl:yOffset()).is(125)
            _:expect(#fl.buffer).is(3)
            _:expect(fl.buffer[1]).is("Message 3")

I think that we’ll get buffer size 4 but the correct message in buffer[1].

3: floater pulls messages appropriately  -- Actual: 4, Expected: 3

Yes. Now the fetch needs to fail to add a message, and to set a flag so as never to return another one if called a few more times.

function Floater:init(provider, yOffsetStart, lineSize, lineCount)
    self.provider = provider
    self.yOffsetStart = yOffsetStart
    self.lineSize = lineSize
    self.lineCount = lineCount
    self.yOff = self.yOffsetStart
    self.buffer = {}
    self.stopFetching = false
    self:fetchMessage()
end

function Floater:fetchMessage()
    if self.stopFetching then return end
    local msg = self.provider()
    if msg == "" then
        self.stopFetching = true
    else
        table.insert(self.buffer, msg)
    end
end

I think this does the job. Tests are green.

Shall we play this out til the end? I think we should. We will need to find some way to stop the drawing, which we have not dealt with at all yet. (Nor, probably, will we.)

Here’s the entire test, which runs green:

        _:test("floater pulls messages appropriately", function()
            local fl = Floater(msg,50,25,4)
            _:expect(#fl.buffer).is(1)
            _:expect(fl.buffer[1]).is("Message 1")
            fl:increment(24)
            _:expect(#fl.buffer).is(1)
            _:expect(fl.buffer[1]).is("Message 1")
            fl:increment()
            _:expect(#fl.buffer).is(2)
            _:expect(fl.buffer[2]).is("Message 2")
            fl:increment(25)
            _:expect(fl.buffer[3]).is("Message 3")
            fl:increment(25)
            _:expect(fl.buffer[4]).is("Message 4")
            
            _:expect(fl:yOffset()).is(125)
            fl:increment(25)
            _:expect(fl:yOffset()).is(125)
            _:expect(#fl.buffer).is(4)
            _:expect(fl.buffer[1]).is("Message 2")
            _:expect(fl.buffer[4]).is("Message 5")
            
            fl:increment(25)
            _:expect(fl:yOffset()).is(125)
            _:expect(#fl.buffer).is(3)
            _:expect(fl.buffer[1]).is("Message 3")
            
            fl:increment(25)
            _:expect(fl:yOffset()).is(125)
            _:expect(#fl.buffer).is(2)
            _:expect(fl.buffer[1]).is("Message 4")
            
            fl:increment(25)
            _:expect(fl:yOffset()).is(125)
            _:expect(#fl.buffer).is(1)
            _:expect(fl.buffer[1]).is("Message 5")
            
            fl:increment(25)
            _:expect(fl:yOffset()).is(125)
            _:expect(#fl.buffer).is(0)
            
        end)

Let’s commit this: floater tests running. All other tests off.

I don’t like committing with tests turned off, but CodeaUnit is a pain to use when you have rafts of tests. Something needs to be done about that. One of these days. Shoemaker’s children. (Look it up.)

Now a break, and some reflection. Maybe we’ll stop for the morning.

Reflection

I chose not to test the rest of the Encounter coroutine, which may have been risky. (It was risky. There are quite possibly defects in there that result from my manual conversion of it. I may be bitten by those defects, and if I am, I promise to state it clearly when it happens, so that you’ll know I was foolish.

Then I turned right around and TDDed the new Floater object as if I were some kind of TDD fanatic. I went in small steps, even did Fake It Till You Make It a couple of times, and it has gone marvelously. I suspect I could plug in a Floater and display something almost perfectly, except for the start/stop monster stuff. In fact, I think I’ll try it.

Back At It

Let’s make a coroutine for the starting scroll. Where is that done now?

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.floater = FloatingMessageList(self)
end

function GameRunner:addMessages(anArray)
    self.floater:addMessages(anArray)
end

...
    self:addMessages(self:initialMessages())
...

~~~lua
function GameRunner:initialMessages()
    return {
    FloatingMessage("Welcome to the Dungeon."),
    FloatingMessage("Here you will find great adventure,"),
    FloatingMessage("fearsome monsters, and fantastic rewards."),
    FloatingMessage("Some of you may die, but I am willing"),
    FloatingMessage("to make that sacrifice.")
    }
end

Oh, that’s why the getText is in there. I forgot that I’d done the floating message wrapper.

Anyway let’s create a coroutine version of that, and see if we can make a Floater display it.

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.")
    coroutine.yield("")
end

Now to patch it in:

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,3)
    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:addMessages(self:initialMessages())
    self.floater = Floater(self.initialCrawl,50,25,4)
end

I expect this to explode looking for Floater:draw.

Not quite:

attempt to yield from outside a coroutine
stack traceback:
	[C]: in function 'coroutine.yield'
	GameRunner:209: in field 'provider'

I forgot to wrap that.

    self.floater = Floater(coroutine.wrap(self.initialCrawl),50,25,4)
GameRunner:159: attempt to call a nil value (method 'draw')
stack traceback:
	GameRunner:159: in method 'drawMessages'
	GameRunner:122: in method 'draw'
	Main:23: in function 'draw'

That’s this:

function GameRunner:drawMessages()
    pushMatrix()
    self:scaleForLocalMap()
    self.floater:draw()
    popMatrix()
end

Let’s just put this in more or less blindly and see what happens:

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

function Floater:numberToDisplay()
    return math.min(#self.buffer, self:linesToDisplay())
end

This actually works!

crawl

There is an issue, or a few. The floater is now going to draw forever, but it will forever find that there are no more lines to draw. That seems semi OK but not ideal. Also I had to fetch the GameRunner from the global, because it isn’t passed in to the creation and it should be.

But it works well enough that I’m going to commit: opening crawl uses Floater with coroutine messages.

That was a bit ragged. Let’s sum up, settle down, and call it a day.

Summary

The TDDing of the Floater went delightfully. I note a few issues, which I’ll put on one of my sticky notes:

  • Clean up creating coroutine. Inside or outside?
  • Cater to replacing runner floater with each new floating message;
  • Remove floater when done;
  • Start/Stop Monsters during floating messages.

Some key enhancements might include:

  • Allow providing a plain array of text to Floater. A standard array-generating coroutine should deal with this.

A larger issue is that I’ve found it desirable to turn off all the tests, and if I turn them back on, my work gets harder. In addition, the current setup doesn’t even display the big red text if the tests fail. Let’s add that to the list as well.

I was feeling like I was rushing there toward the end, when I patched the floater in. No harm done, I think, but it’s not good to run very far with your arms windmilling to keep from falling over, and it’s not good to rush when coding for much the same reason.

Still, the coroutine approach to messages is looking pretty good. I think I’ll be glad I did it, but I freely grant that it has taken more hours than I’d like, almost a whole working day. Nonetheless, we’ve managed to get our code committed and to keep the system working throughout. We were shippable all the time.

See you next time!


D2.zip