Let’s advance the game, and try to advance our ‘making’ tech at the same time. This just in: Bear bites Ron.

In my copious free time, reflecting on the notion of a “Making App” for a Codea/Lua project, I begin to think that I’ve been thinking too far ahead, with grand ideas about how to move masses of code in and out of the Shipping App, or use of dependencies, and various technical do-dads and baubles. That’s not the way to do it.

The thing, I believe, is to do a not-quite-sufficient cut at a tool when we actually have need of it, not before. So we’ll try to do that. So let’s begin with a story, a feature, or even a fix to the Shipping App, and see what happens.

I’ll do a quick review of my pile of sticky notes here, and list the ones that seem interesting:

  • In Making App output, fix the blank long line issue;
  • Check whether dungeon is “legit”;
  • Better plan for Spike and Lever;
  • Doors, keys, levels, traps, puzzles, story;
  • Gold / Treasure and a Store;
  • Rename _variable names;
  • __newIndex and __index for globals
  • Player startActionWithMonster has if statement;
  • Quality check Monster tables;
  • Darkness / Torches;
  • It’s a pain when there are lots of tests;

Let’s take a quick look at that last one. The issue is that when the tests run, they dump a fair amount of info to the console:

tests

As you can see, there are three separate chunks of text output for each test, the loading message, the feature message, and the results summary. And Codea displays each of those with a double space between.

When a test fails, its failure messages are up there somewhere amid all that clutter, and I have to scroll around to find the message in order to understand and fix the problem. Since a typical fix takes me a few tries, I wind up doing that more than once. It’s a pain.

Let’s take a quick review of how CodeaUnit works as regards print. Here’s the main offender:

function CodeaUnit:describe(feature, allTests)
    self.tests = 0
    self.ignored = 0
    self.failures = 0
    self._before = function()
    end
    self._after = function()
    end
    
    local f_string = string.format("Feature: %s", feature)
    CodeaUnit._summary = CodeaUnit._summary .. f_string .. "\n"
    print(f_string)
    
    allTests()
    
    local passed = self.tests - self.failures - self.ignored
    local summary = string.format("%d Passed, %d Ignored, %d Failed", passed, self.ignored, self.failures)
    CodeaUnit._summary = CodeaUnit._summary .. summary .. "\n"
    print(summary)
end

Here we see printing the Feature line and the summary line. The loading message comes out here:

CodeaUnit.execute = function()
    CodeaUnit._summary = ""
    for i,v in pairs(listProjectTabs()) do
        local source = readProjectTab(v)
        for match in string.gmatch(source, "function%s-(test.-%(%))") do
            print("loading", match)
            load(match)() -- loadstring pre Lua 5.4
        end
    end
    return CodeaUnit._summary
end

There’s already one flag, CodeaUnit.detailed, that is used to suppress the OK messages and some others. We could just comment out the prints that we don’t like, since we have a copy of CodeaUnit embedded in the app. Truth be told, I think I’ll do that. We could set up another flag and such but for now let’s put in a comment that we can find later.

function CodeaUnit:describe(feature, allTests)
    self.tests = 0
    self.ignored = 0
    self.failures = 0
    self._before = function()
    end
    self._after = function()
    end
    
    local f_string = string.format("Feature: %s", feature)
    CodeaUnit._summary = CodeaUnit._summary .. f_string .. "\n"
    --[[ print(f_string) -- silence console --]]
    
    allTests()
    
    local passed = self.tests - self.failures - self.ignored
    local summary = string.format("%d Passed, %d Ignored, %d Failed", passed, self.ignored, self.failures)
    CodeaUnit._summary = CodeaUnit._summary .. summary .. "\n"
    --[[ print(summary) -- silence console --]]
end

That’ll do for now, and the comment should help us find the change if we need to later. Maybe we’ll enhance the basic CodeaUnit one day.

Let’s break a test and see what happens. Even before I break a test, I run them and all that comes out is “test time 0.0”. I think I’d really rather have at least some indication that tests ran. Let’s look at the execute method.

CodeaUnit.execute = function()
    CodeaUnit._summary = ""
    for i,v in pairs(listProjectTabs()) do
        local source = readProjectTab(v)
        for match in string.gmatch(source, "function%s-(test.-%(%))") do
            --[[ print("loading", match) -- silence console --]]
            load(match)() -- loadstring pre Lua 5.4
        end
    end
    return CodeaUnit._summary
end

Not much help there. The individual describe functions zero out test count and errors and such. We can’t really see those from here.

In draw we do this:

function draw()
    local showingTests = false
    --speakCodeaUnitTests()
    if CodeaUnit and CodeaTestsVisible then
        if Console:find("[1-9] Failed") 
            or Console:find("[1-9] Ignored")
            or Console:find("[1-9][0-9] Failed")
            or Console:find("[1-9][0-9] Ignored") then
            showingTests = true
            showCodeaUnitTests()
        end
    end
    if not showingTests then 
        background(0)
        Runner:draw()
    end
end

If our patterns find Fails or Ignores, we show the tests on the big screen. Otherwise we do nothing. No help here.

Up in setup:

function setup()
    Sprites:initializeSprites()
    if CodeaUnit then 
        local t = os.time()
        runCodeaUnitTests() 
        CodeaTestsVisible = true
        print("test time ", os.difftime(os.time(),t))
    end
...

Maybe if we just improve that message to make it clear that the tests were run.

        print("CodeaUnit Tests:\nTest Time ", os.difftime(os.time(),t))

OK, enough. Let’s break a test now.

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

perfect

Perfect. The big screen shows the summary, and the console shows just the error and the final timing summary. Commit: CodeaUnit tests now show only errors in console.

Good. That has been bugging me just a bit for a long time. Maybe not the perfect solution, but a pretty decent cut at it.

What Next?

That one about the if statement made me curious:

function Player:startActionWithMonster(aMonster)
    local name = aMonster:name()
    if name ~= "Mimic" or (name == "Mimic" and  aMonster.awake) then
        self.runner:initiateCombatBetween(self,aMonster)
    else
        aMonster.awake = true
    end
end

What we have here is a special deal for the Mimic. The Mimic starts out asleep (not awake) and if we bump a sleeping mimic, we wake it up, and nothing else. Otherwise, when we bump a monster we’re initiating combat with it.

If the Mimic were a different class, we could make it respond differently to a first attack, but that seems like a lot of mechanism compared to this if. So I’m going to let that slide. But I’ll keep the sticky note in case someone comes up with a better idea.

The one about a quality check on the monster tables seems important. Here’s what they look like:

function Monster:initMonsterTable()
    local skip = 0 -- 0 or 10 hack to make all mimics
    local m
    MT = {}
    m = {name="Pink Slime", level = skip+1, health={1,2}, speed = {4,10}, strength=1,
        attackVerbs={"smears", "squishes", "sloshes at"},
        dead={asset.slime_squashed}, hit={asset.slime_hit},
        moving={asset.slime, asset.slime_walk, asset.slime_squashed}}
    table.insert(MT,m)
    m = {name="Mimic", level=1, health={10,10}, speed={10,10}, strength=10, facing=-1, strategy=MimicMonsterStrategy, behaviorName="mimicBehavior",
        attackVerbs={"bites", "chomps", "gnaws"},
        dead={"mdead01","mdead02","mdead03","mdead04","mdead05","mdead06","mdead07","mdead08","mdead09","mdead10"},
        hit={"mhurt01", "mhurt02", "mhurt03", "mhurt04", "mhurt05", "mhurt06", "mhurt07", "mhurt08", "mhurt09", "mhurt10"},
        moving={"mwalk01", "mwalk02", "mwalk03", "mwalk04", "mwalk05", "mwalk06", "mwalk07", "mwalk08", "mwalk09", "mwalk10"},
        attack={"mattack01", "mattack02", "mattack03", "mattack04", "mattack05", "mattack06", "mattack07", "mattack08", "mattack09", "mattack10"},
        idle={"midle01", "midle02", "midle03", "midle04", "midle05", "midle06", "midle07", "midle08", "midle09", "midle10"},
        hide={"mhide01", "mhide02", "mhide03", "mhide04", "mhide05", "mhide06", "mhide07", "mhide08", "mhide09", "mhide10"},
    }
    table.insert(MT,m)
    m = {name="Death Fly", level = skip+1, health={2,3}, speed = {8,12}, strength=1,
        attackVerbs={"bites", "poisons"},
        dead={asset.fly_dead}, hit={asset.fly_hit},
        moving={asset.fly, asset.fly_fly}}
    table.insert(MT,m)
    m = {name="Ghost", level=skip+1, health={1,5}, speed={5,9},strength={1,1},
        attackVerbs={"licks", "terrifies", "slams"},
        dead={asset.ghost_dead}, hit={asset.ghost_hit},
        moving={asset.ghost, asset.ghost_normal}}
    table.insert(MT,m)
    m = {name="Toothhead", level = 2, health={4,6}, speed = {8,15}, strength={1,2},
        attackVerbs={"gnaws at", "bites", "sinks teeth into"},
        dead={asset.barnacle_dead}, hit={asset.barnacle_hit},
        moving={asset.barnacle, asset.barnacle_bite}}
    table.insert(MT,m)
    m = {name="Vampire Bat", level=2, health={3,8}, speed={5,10}, strength={8,10},
        attackVerbs={"drains", "bites", "fangs"},
        dead={asset.bat_dead}, hit={asset.bat_hit},
        moving={asset.bat, asset.bat_fly}}
    table.insert(MT,m)
    m = {name="Murder Hornet", level = 2, health={2,3}, speed = {8,12}, strength={2,4},
        attackVerbs={"stings", "poisons", "jabs"},
        dead={asset.bee_dead}, hit={asset.bee_hit},
        moving={asset.bee, asset.bee_fly}}
    table.insert(MT,m)
    m = {name="Serpent", level=3, health={8,14}, speed={8,15}, strength={8,12},
        attackVerbs={"bites", "poisons", "strikes at"},
        dead={asset.snake_dead}, hit={asset.snake_hit},
        moving={asset.snake, asset.snake_walk}}
    table.insert(MT,m)
    m = {name="Yellow Widow", level=3, health={1,4}, speed = {2,5}, strength={9,15},
        attackVerbs={"bites", "poisons", "tangles"},
        dead={asset.spider_dead}, hit={asset.spider_hit},
        moving={asset.spider, asset.spider_walk1,asset.spider_walk2}}
    table.insert(MT,m)
    m = {name="Poison Frog", level=3, health={4,8}, speed = {2,6}, strength={8,11},
        attackVerbs={"leaps at", "smears poison on", "poisons", "bites"},
        dead={asset.frog_dead}, hit={asset.frog_hit},
        moving={asset.frog, asset.frog_leap}}
    table.insert(MT,m)
    m = {name="Ankle Biter", level=4, health={9,18}, speed={3,7}, strength={10,15},
        attackVerbs={"grinds ankles of", "cuts ankle of", "saws at feet of"},
        dead={asset.spinnerHalf_dead}, hit={asset.spinnerHalf_hit},
        moving={asset.spinnerHalf, asset.spinnerHalf_spin}}
    table.insert(MT,m)
    m = {name="Cloud", level=99, health={20,20}, speed={20,20}, strength={20,20},
        attackVerbs={"wafts"}, dead={asset.whitePuff00}, hit={asset.whitePuff01},
    moving={asset.whitePuff00, asset.whitePuff01, asset.whitePuff02, asset.whitePuff03}}
    table.insert(MT,m)
end

So, yeah, there’s a lot to get wrong in there. Let’s make a start at a quality check. I think a class method on Monster.

No. What a dolt! This should be a CodeaUnit test.

        _:test("MonsterEntry table quality", function()
            local MT = Monster:initMonsterTable()
            for i,mte in ipairs(MT) do
                checkMTentry(i,mte)
            end
        end)

function checkMTentry(num, mte)
    local name = mte.name
    _:expect(name,"monster "..num.." has no name").isnt(nil)
    _:expect(#mte.strength,name.." strength").is(2)
end

This errors because we’re allowing some strength entries to be numbers.

3: MonsterEntry table quality -- Monsters:48: attempt to get length of a number value (field 'strength')

I’d like to know where the error is. Thought I had enough info in the expect but not quite.

I’ll just go fix them all. I think I got them, but let’s beef up the test anyway.

function checkMTentry(num, mte)
    local name = mte.name
    _:expect(name,"monster "..num.." has no name").isnt(nil)
    local temp = mte.strength
    _:expect(type(temp),name.." strength").is("table")
    _:expect(#mte.strength,name.." strength").is(2)
end

Test runs. Let’s do health and speed similarly. Test continues to run.

Everyone needs at least one animation for dead, hit and moving. And I see duplication in the test, and it’s not helpful duplication.

function checkMTentry(num, mte)
    local temp
    local name = mte.name
    _:expect(name,"monster "..num.." has no name").isnt(nil)
    checkIsTable(mte,name,"health", 2)
    checkIsTable(mte,name,"strength", 2)
    checkIsTable(mte,name,"speed", 2)
end

function checkIsTable(mte, name, tabName, length)
    local t = mte[tabName]
    _:expect(type(t), name.." "..tabName).is("table")
    if length then
        _:expect(#t, name.." length "..tabName).is(length)
    end
end

Now let’s check the animations:

function checkMTentry(num, mte)
    local temp
    local name = mte.name
    _:expect(name,"monster "..num.." has no name").isnt(nil)
    checkIsTable(mte,name,"health", 2)
    checkIsTable(mte,name,"strength", 2)
    checkIsTable(mte,name,"speed", 2)
    checkIsTable(mte,name,"moving")
    checkIsTable(mte,name,"hit")
    checkIsTable(mte,name,"dead")
end

We don’t provide a number to those last three, so we check that it is a table, but not its length. So far everything’s passing. That’s good news, and maybe it’s not a waste of time.

I think, for now, that’s a good start. Let’s commit: added some quality tests for initMonsterTable.

Now What?

I seem to be in a mode of doing little things. Let’s do another, the variable names that start with underbar. I had in mind that those would be private or some such, but I didn’t stick with the plan, so now they’re just irritating.

I’m using them in some tests, with the save locations, such as _bus. I’m OK with that, it’s the ones in the actual code that bug me.

Monster has one, _movementStrategy. I see no reason for that to be allowed to exist. Rename. Test. All good. Commit: rename _movementStrategy - _.

OK. This is boring. All easy, all going smoothly. We need something tricky.

Doors

OK, doors. Let’s get started on doors. The basic idea will be that some rooms have doors and a door requires, and consumes, a key in order to open it.

The issue with doors is this: we don’t know where they should go. Remember how we draw the dungeon. We place random rooms, allowing them to be tangent if the numbers roll that way. Then we draw connections from room 1 to 1, 3 to 2, 4 to 3, randomly choosing to move first horizontally and then vertically, from the starting room center to the ending room center. So far so good but remember what actually happens:

rooms

In the picture above, the circled opening is not an “official” door to the room. I think the openings at 3 and 6 o’clock are the real ones. In any case, no “official” door is two tiles wide. At least half that path was drawn to connect some other rooms.

Bottom line, at present, the game literally does not know where the openings are to the rooms. All we know is that all the rooms are connected by at least one path: there’s no room that is unconnected. There can be rooms with only one connection. The one at the top right is an example. It’s not guaranteed to happen, though at a guess it happens more than half the time.

So here’s my cunning plan.

What if, when we draw a hallway, we were to detect that we are at the outer rim of a room, and mark that tile as a potential door, perhaps even informing the Room object where it is being entered. Then the basic info would be there as to the tiles we’d have to block if we wanted to put in doors, and we could assess rooms to see if they had just one entrance and, well, whatever else we wanted.

I can think of at least two ways to approach this:

  1. When we draw the hallways, check each tile’s position against all the room boundaries and if we’re on one, take our marking action.
  2. When we draw the rooms, mark their border tiles differently from their inner tiles and detect that during hallway drawing.

Well, when you put it that way, it’s pretty obvious that the second idea is better.

Back in the distant past, we used to do a similar thing: we created the rooms with wall tiles around them. Somewhere along the way it became clear that we didn’t need to do that, which gave us the nice room layouts we get now, where two adjacent rooms can produce nice odd shapes.

But we could surely mark the outer room tiles somehow.

That’s probably in Room:paint.

function Room:paint()
    for x = self.x1,self.x2 do
        for y = self.y1,self.y2 do
            self.runner:defineTile(self:correctTile(x,y))
        end
    end
end

function Room:correctTile(x,y)
    return Tile:room(x,y, self.runner)
end

The defineTile method just puts the tile in the dungeon, so we can mark them here if we wish.

Let’s see. When is a tile on the border of the room? Looks to me that if x = x1 or x2 or y = y1 or y2, it’s a border tile.

I’m not feeling TDDing this. I’m a bad person, you should never try this at home.

Extract expression to temp:

function Room:paint()
    for x = self.x1,self.x2 do
        for y = self.y1,self.y2 do
            local tile = self:correctTile(x,y)
            self.runner:defineTile(tile)
        end
    end
end

OK, I can do this … no, I can’t bring myself to test this. It’s too trivial. Nothing could go wrong … go wrong … go wrong …

function Room:paint()
    for x = self.x1,self.x2 do
        for y = self.y1,self.y2 do
            local tile = self:correctTile(x,y)
            local isBorder = x==self.x1 or x==self.x2 or y==self.y1 or y==self.y2
            tile:setBorder(isBorder)
            self.runner:defineTile(tile)
        end
    end
end

And in Tile:

function Tile:initDetails()
    self.contents = DeferredTable()
    self.seen = false
    self:clearVisible()
    self.sprite = nil
    self.border = false
end

function Tile:isBorder()
    return self.border
end


function Tile:setBorder(boolean)
    self.border = boolean
end

To test this, I’ll do this:

function Tile:drawSprites(tiny)
    local center = self:graphicCenter()
    if tiny then
        self:drawMapCell(center)
    else
        if self:isBorder() then
            color(255,0,0)
        end
        self:drawLargeSprite(center)
    end
end

Yeah and I don’t see any red cells. I told you I didn’t need to test drive this.

OK, the bug is color. And it’s in the wrong place.

function Tile:drawLargeSprite(center)
    -- we have to draw something: we don't clear background. Maybe we should.
    local sp = self:getSprite(self:pos(), false)
    pushMatrix()
    pushStyle()
    textMode(CENTER)
    translate(center.x,center.y)
    if not self.currentlyVisible then tint(0) end
    if self:isBorder() then
        tint(255,0,0)
    end
    sp:draw()
    popStyle()
    popMatrix()
end

red room

OK, that works as intended. And it also points out an issue with this idea. It’s pretty clear that the door doesn’t want to be on the border, it wants to be outside it.

I’m going to revert this idea and try to think of a new one.

Plan B

OK, where should a door go on the picture above? Depending on which direction the hall is going, outbound or inbound, it should go onto

  • The first non-room tile encountered, if outbound
  • The last non-room tile encountered, if inbound

The former is easy. The latter maybe not so much.

Se do have some tests for the hv methods, I think.

Here’s a typical one:

        _:test("Horizontal corridor", function()
            local tile
            local msg
            local dungeon = Runner:getDungeon()
            dungeon:horizontalCorridor( 5,10, 7)
            tile = Runner:getTile(vec2(4,7))
            _:expect(tile:isEdge()).is(true)
            tile = Runner:getTile(vec2(11,7))
            _:expect(tile:isEdge()).is(true)
            for x = 5,10 do
                tile =  Runner:getTile(vec2(x,7))
                msg = string.format("%d %d", x, 7)
                _:expect(tile:isRoom(), msg).is(true)
            end
        end)

But there’s a better way of checking used in the others:

        _:test("backward horizontal corridor", function()
            local tile
            local msg
            local r,x,y
            local dungeon = Runner:getDungeon()
            dungeon:horizontalCorridor( 10,5, 7)
            r,x,y = checkRange(dungeon, 5,7, 10,7, Tile.isRoom)
            msg = string.format("%d %d", x, y)
            _:expect(r,msg).is(true)
        end)

function checkRange(dungeon, x1, y1, x2, y2, checkFunction)
    x1,x2 = math.min(x1,x2), math.max(x1,x2)
    y1,y2 = math.min(y1,y2), math.max(y1,y2)
    for x = x1,x2 do
        for y = y1,y2 do
            local t = dungeon:privateGetTileXY(x,y)
            local r = checkFunction(t)
            if not r then
                local msg = string.format("checkRange %d,%d fails", x,y)
                _:expect(r,msg).is(true)
                return r,x,y
            end
        end
    end
    _:expect(true).is(true)
    return true,0,0
end

We could have used that function in the first test I showed. And we should: otherwise we’ll look at that one and wonder why we didn’t use it.

        _:test("Horizontal corridor", function()
            local tile
            local msg
            local dungeon = Runner:getDungeon()
            dungeon:horizontalCorridor( 5,10, 7)
            tile = Runner:getTile(vec2(4,7))
            _:expect(tile:isEdge()).is(true)
            tile = Runner:getTile(vec2(11,7))
            _:expect(tile:isEdge()).is(true)
            r,x,y = checkRange(dungeon, 5,7, 10,7, Tile.isRoom)
            msg = string.format("%d %d", x, y)
            _:expect(r,msg).is(true)
        end)

Tests run. Commit: use checkRange in horizontal corridor test.

Let’s look at the actual operating methods. They actually reside now in Dungeon. I think that’s a change.

function Dungeon:horizontalCorridor(fromX, toX, y)
    fromX,toX = math.min(fromX,toX), math.max(fromX,toX)
    for x = fromX,toX do
        self:setHallwayTile(x,y)
    end
end

function Dungeon:verticalCorridor(fromY, toY, x)
    fromY,toY = math.min(fromY,toY), math.max(fromY,toY)
    for y = fromY, toY do
        self:setHallwayTile(x,y)
    end
end

function Dungeon:setHallwayTile(x,y)
    local t = self:privateGetTileXY(x,y)
    if not t:isRoom() then
        self:defineTile(Tile:room(x,y,t.runner))
    end
end

We don’t do anything with existing room tiles at this point: we just define non-room tiles to be room tiles.

I’m still struggling to think of a good way to do this. What about drawing both the horizontal and vertical component of a path outward, that is, toward the corner of the path. Then in each case we could have a one-time state flag that tells us whether we have started laying down new room tiles, and we could mark the first one with the doorway signal.

Right now, our horizontal and vertical corridor drawing just draws from low to high, but it’s called knowing from and to. It just reverses them so it can always draw forward.

Let’s back out that decision, and draw always from the “from” position to the “to” position. Our tests should be robust enough to tell us if we get it wrong.

function Dungeon:horizontalCorridor(fromX, toX, y)
    local increment = 1
    if fromX > toX then
        increment = -1
    end
    for x = fromX,toX,increment do
        self:setHallwayTile(x,y)
    end
end

That looks OK to me. Test. Good. Now the other. This one I’m going to do intentionally wrong to be sure the tests barf.

function Dungeon:verticalCorridor(fromY, toY, x)
    local increment = 1
    if fromY>toY then
        increment = 1
    end
    for y = fromY, toY, increment do
        self:setHallwayTile(x,y)
    end
end
4: connect rooms h then v checkRange 3,5 fails -- Actual: false, Expected: true
5: connect rooms v then h checkRange 11,4 fails -- Actual: false, Expected: true

Was there no plain horz or vert test that should have failed? No, there wasn’t, just the one. Change the code, tests should run.

function Dungeon:verticalCorridor(fromY, toY, x)
    local increment = 1
    if fromY>toY then
        increment = -1
    end
    for y = fromY, toY, increment do
        self:setHallwayTile(x,y)
    end
end

Tests are green. Commit: do all corridors, from->to

Now we need to think a bit more.

Naively, if we were to write these two loops so that while the tile at x,y is a room tile, do nothing, then when it first isn’t, set it to doorway and from there on, lay down hallway (which is really room type).

But suppose as we’re carving along, we leave our starting room, bing, lay down a door, then find ourselves about to enter another room, i.e. the tile in front of us is already room type. Should we mark our entry tile as doorway? In principle, I think we could.

I also think that I’d be very wise to figure out a TDD approach to this, since we can switch states often, and sometimes we’re wanting to look back (was the previous tile room) and other times look forward (is the one we’re about to do room) and if that sounds right to you, you don’t understand the problem yet. Or I don’t. I’m sure I don’t.

OK, I’m just going to try to write a new test among these dungeon ones.

Let’s imagine that we’re marching along, marking out a hallway. We start in a room, move in some H or V direction. As long as we are on room tiles, we do nothing. When we are in this state (looking for non-room), when we see a non-room tile, we can check the previous tile, and if it is room, then mark this tile “door”, and set it to room type, and change to the other state, looking for room.

In looking for room state, we look at the current tile, and if it is non-room, mark it room and carry on. If it is room, mark the previous tile as door, and enter looking for non-room state.

Does that sound right?

This makes me want a little state-bearing object that will tell me what to do. Given two tiles, current and previous, should I mark current as door, should I mark current as room, and should I mark previous as door?

Should we have this object tell us what to do, or should it do it? I guess given my style, the answer is that it should do it. That does make it harder to test, however. We’ll have to give it some kind of callback.

Let’s see if we can do a table of what we expect

State Current Prev Set Curr Set Prev State
LFNR R R _ _  
LFNR N R room _  
      door _ LFR
LFR N x room _ LFR
LFR R x _ door LFNR

There’s a useful invariant here, which is that we never set a room tile to room, and we always set a non-room tile to room. So that seems to me that we only need to consider whether we should set some tile to door. Maybe this table:

State Current Prev Set Curr Set Prev State
LFNR N R door _ LFR
LFR R N _ door LFNR

One issue is that in LFR mode, the previous tile will already have been set to room, so we need to be remembering whether it was room before we passed through it.

This is rather intricate isn’t it. Let’s try to express our needs as some tests.

        _:test("mark doorways", function()
            local seq
            seq = "RRRNNN"
            ret = "RRRdrr"
            _:expect(markDoors(seq)).is(ret)
        end)

Here I’ve created a string representation of the stream of tiles we’ll see, and I’m saying that our function sets N tiles either to d or to r. This example is an outbound path that hits no other rooms.

What the heck am I doing, you ask? I’m working to figure out what has to happen, and I’m devising a representation that I can understand. Will I use that in the end? I don’t know.

Basically I’m trying to trim down this problem so as to need to keep as little of it in my head as possible, with the rest in the tests and code.

Now could I write markDoors?

This is a lot to have typed in but I did:

function markDoors(seq)
    local prev, curr
    local ret = "R"
    local state = "LFNR"
    prev = seq[1]
    for i = 2,#seq do
        ret = ret..mark(prev,curr, state)
    end
end

function mark(prev,curr,state)
    if state == "LFNR" then
        if curr == "R" then return "R" end
        state = "LFR"
        if prev == "R" then
            return "d"
        else
            return "r"
        end
    else -- LFR
        if curr == "N" then return "r" end
        error("noooo")
    end
end

Run and see what fails.

6: mark doorways  -- Actual: nil, Expected: RRRdrr

Right. Helps to return the result.

function markDoors(seq)
    local prev, curr
    local ret = "R"
    local state = "LFNR"
    prev = seq[1]
    for i = 2,#seq do
        ret = ret..mark(prev,curr, state)
    end
    return ret
end
6: mark doorways  -- Actual: Rrrrrr, Expected: RRRdrr

Not so good.

Probably would help if I were to set up prev and curr.

function markDoors(seq)
    local prev, curr
    local ret = "R"
    local state = "LFNR"
    prev = seq[1]
    for i = 2,#seq do
        curr = seq[i]
        ret = ret..mark(prev,curr, state)
        prev = curr
    end
    return ret
end

See what I mean about not keeping things in my head? There are at least seven holes in it.

Curiously, that change doesn’t change anything. Oh. In Lua a string doesn’t act like an array of char. We’ll have to do this:

function markDoors(seq)
    local prev, curr
    local ret = "R"
    local state = "LFNR"
    prev = seq:sub(1,1)
    for i = 2,#seq do
        curr = seq:sub(i,i)
        ret = ret..mark(prev,curr, state)
        prev = curr
    end
    return ret
end

Test runs.

Let’s try a harder test.

            seq = "RRNNNNRRR"
            ret = "RRdrrdRRR"
            _:expect(markDoors(seq)).is(ret)
6: mark doorways  -- Actual: RRdrrrRRR, Expected: RRdrrdRRR

I don’t think I’ve catered to the setting of d. And now that we’re here, I don’t see quite what to do, because we’ve already processed prev. So we can’t quite do this by generating one at a time, can we?

I can’t quite make this work, yet. It’s past time to stop work. I rather like the test, I just haven’t figure out how to implement the code, mostly because the string manipulation gets weird, and Lua passes strings by value, not reference. I didn’t know that. It passes tables by reference, everything else by value. Very clever, these Lua people.

Pigheaded, I try one more time and get closer to what I want. Maybe one more entry in my function:

No. I can’t change the state the way I was doing it, because it, too, is passed by value. I’‘m going to need to write a little object for this. For now, let’s remove the code and ignore the test, which looks to me to be potentially useful.

I have an abrasion on my cornea, so being as it is about 2PM, well after hours anyway, I’m going to call it a day.

I think I’m getting a sense of what has to be done, and have made a little progress toward it by virtue of always moving outward from the room in the hallway carving.

But, basically, the bear bit me today. Tomorrow, however, I’m gonna bite it back.

See you next time!


D2.zip