Just a little bit more, for an afternoon’s relaxation.

I’m feeling the desire to do a bit more today, rather than read Wanderers or something even less fruitful, like pay attention to the news. So let’s see what we can do that’s more or less on the list.

  • Implement the stop-start monsters flag;
  • Deal properly with additional functions coming into Floater while one is already running;
  • Remove the old Encounter and its tests;
  • Make sure all the tests are running;
  • … other things we may find …

The stop start flag should be easy enough, I think we have an explicit start and stop point now. This should be a feature of Floater, I believe, as it was a feature of FloatingMessageList.

The stop is easy:

function Floater:startCrawl()
    self.yOff = self.yOffsetStart
    self.buffer = {}
    self.runner:stopMonsters()
    self:fetchMessage()
end

Stopping should also be pretty clear. It should be done when we discover that the coroutine is dead and the buffer is empty. FloatingMessageList did it like this:

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

However, our version of that function is a bit more complicated:

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

We can almost certainly put the check last in this method. If the coroutine is dead or dies, we will not add a line to the buffer, so its size will still be zero.

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
    if #self.buffer == 0 then
        self.runner:startMonsters()
    end
end

I’ll quickly check this, which I can do by trying to move the Princess while the crawl is crawling. It works, and I even tried a battle. The effect in a battle is troubling, in that I’ve developed the habit of moving away from a monster, then darting in and back out, letting the encounter run to completion, but with the certainty that the monster can’t attack me before I’m ready. That approach doesn’t really work now, though I can of course wait for the crawl to finish and then dart away, or just keep tapping the move key a lot.

Anyway, commit: Stop-start Monsters works.

Removing Encounter

I should be able to remove Encounter, and the tests for it, and the FloatingMessage and FloatingMessage list as well.

Let’s give that a go.

The TestEncounter tab has a test for the old Encounter class. I’ll remove the class tab and let the tests tell me what needs to be removed there.

1: Encounter with player faster -- TestEncounter:31: attempt to call a nil value (global 'Encounter')

Remove that test.

2: Encounter with monster faster -- TestEncounter:47: attempt to call a nil value (global 'Encounter')

Remove that test. Remove two more by inspection.

However, a few other things have crept in. First, the stop start monsters can’t be done if we don’t have a runner, and our tests don’t provide one. I condition both those calls with if statements. A bit messy but we’ll consider that when the tests are green. Make it work, then make it right.

Still one failing:

1: coroutine encounter, player faster -- attempt to yield from outside a coroutine

Whazzup with that?

        _:test("coroutine encounter, player faster", function()
            local msg
            randomNumbers = {3, 2, 3,2, 1}
            local mtile = Tile:room(10,10, runner)
            local monster = Monster(mtile,runner,Monster:getMtEntry(1))
            local ptile = Tile:room(11,10, runner)
            local player = Player(ptile,runner)
            local co = createEncounter(player, monster,fakeRandom)
            msg = co()
            _:expect(msg).is("Princess attacks Pink Slime!")
            msg = co()
            _:expect(msg).is("Princess is faster!")
            msg = co()
            _:expect(msg).is("Princess strikes!")
        end)

Ah. We’re not wrapping the encounter’s coroutine any more. And createEncounter just returns a function. So …

        _:test("coroutine encounter, player faster", function()
            local tf,msg
            randomNumbers = {3, 2, 3,2, 1}
            local mtile = Tile:room(10,10, runner)
            local monster = Monster(mtile,runner,Monster:getMtEntry(1))
            local ptile = Tile:room(11,10, runner)
            local player = Player(ptile,runner)
            local co = coroutine.create(createEncounter(player, monster,fakeRandom))
            tf,msg = coroutine.resume(co)
            _:expect(msg).is("Princess attacks Pink Slime!")
            tf,msg = coroutine.resume(co)
            _:expect(msg).is("Princess is faster!")
            tf,msg = coroutine.resume(co)
            _:expect(msg).is("Princess strikes!")
        end)

Not much of a test, but it’s green. The TestEnounter tab is now just that one test.

Now I think we can remove FML and FloatingMessage and the corresponding tests. That causes one problem:

GameRunner:18: attempt to call a nil value (global 'FloatingMessageList')
stack traceback:
	GameRunner:18: in field 'init'
	... false

We don’t need the FML in GameRunner any more, and in fact, we can’t have one. A few deletes, including the one that was here:

function GameRunner:drawMessages()
    pushMatrix()
    self:scaleForLocalMap()
    if self.cofloater then self.cofloater:draw() end
    popMatrix()
end

We now always have a cofloater, so I should be able to remove that if, but first let’s make sure all the tests are running. They are. However, in testing that, I chose to take a little wander in the dungeon and encountered an important previously-known problem:

stalled

The Princess has killed a Death Fly in a hallway. The current system settings will not allow her to step over the dead monster. We do have a feature that allows that. Let’s see about turning it back on.

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

We can allow entry into a dead monster cell this way:

    t[Monster][Player] = {moveTo=TileArbiter.refuseMoveIfResidentAlive, action=Player.startActionWithMonster}

I should have committed before doing that, since I have removed all those FML tabs. Now I’ve got to check that feature. Hold on … yes, that works as intended.

Commit: remove FloatingMessage objects and tests. Allow player to step into dead monster’s tile.

I’m noticing another larger concern: the Princess is too vulnerable compared to the monsters. This isn’t a surprise: some of them are intentionally pretty strong, and are intended for use in lower levels of the dungeon (and therefore higher levels of experience in the player). However, I am beginning to think that we shouldn’t just kill off the princess with a sort of permadeath, but instead knock her out until she heals or something.

That will definitely be for another day.

For now, we’ve added two code tabs, Floater and EncounterCoroutines, and removed a bunch of tests and the FloatingMessage, FloatingMessageList, and Encounter tabs.

The system is net simpler in lines of code and tests, at the cost of the added complexity of the coroutine. However, I see no good way to have accomplished our story, which was to ensure that the action on the screen doesn’t get too far ahead of the action in the information crawl.

Now what about that if in the draw:

function GameRunner:drawMessages()
    pushMatrix()
    self:scaleForLocalMap()
    if self.cofloater then self.cofloater:draw() end
    popMatrix()
end

We can remove that:

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

Commit: cofloater drawn unconditionally.

Let’s protect the Floater against unexpected overrides:

function Floater:runCrawl(aFunction)
    self.provider = coroutine.create(aFunction)
    self:startCrawl()
end

We’ll just ignore new inputs if the current provider isn’t dead:

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

Commit: can’t start a crawl while one is running.

Let’s sum up.

Summary

This afternoon felt smoother than this morning, and now that the coroutine is pretty solid and the excess tabs are removed, I’m feeling better about the whole system.

However, today is Wednesday, and we started on this idea … on Sunday, so we have probably eight hours or more invested. That’s rather a lot, and the road was a bit bumpy.

That said, the problem we set out to solve was a difficult one, and however we undertook it, it was slated to cause some trouble.

In a “real” game development, we would probably be working on mob AI, with either finite state machines or behavior trees. Right now, our “AI” is hand-coded into our encounter coroutine, and it’s pretty simple. I believe I’ve promised to work on a behavior tree, and I often keep my promises. We’ll look at that as we devise new stories for the dungeon.

Before that, I think we need to do some simpler things, including auto-healing for the Princess, and some more treasures. And we need to code speed into the system: right now it’s basically a constant, same for everyone.

All that will be for tomorrow and the days beyond. I hope you’ll be following along!


D2.zip