Variations on an Idea from the Wild Blue Onder. No success.

Responding to this morning’s article about the DevMap, Bruce Onder tweeted:

What do you think of this idea: pull out the drawing stuff into its own rendering object, initialized with a “null object” which draws the game in default mode (mini map, zoomed in level).

Then when the ShowMap is disabled, call the renderer, replacing with the NoMap object?

That got me thinking instead of napping, as had been my intention, so now I’m here at 1630, thinking I’ll consider some ways of getting rid of those if statements.

Here are all the references to DevMap:

function setup()
    if CodeaUnit then 
        runCodeaUnitTests() 
        CodeaTestsVisible = true
    end
    parameter.boolean("ShowMap",false)
    parameter.boolean("DevMap", false)
    ...

function GameRunner:drawTinyMapOnTopOfLargeMap()
    if DevMap then return end
    pushMatrix()
    self:scaleForTinyMap()
    Runner:drawMap(true)
    popMatrix()
end

function GameRunner:scaleForLocalMap()
    if DevMap then
        self:getDungeon():setVisibility()
        translate(0,0)
        local hs = WIDTH/(self.tileSize*(1+self.tileCountX))
        local vs = HEIGHT/(self.tileSize*(1+self.tileCountY))
        local s = math.min(vs,hs)
        scale(s)
    else
        local center = self.player:graphicCorner()
        translate(WIDTH/2-center.x, HEIGHT/2-center.y)
        scale(1)
    end
end

function Player:drawExplicit(tiny)
    if tiny and DevMap then return end
    local sx,sy
    ...

function AttributeSheet:draw()
    if DevMap then return end
    local m = self.monster
    ...

Now I’m not certain exactly what Bruce was suggesting, but the basic idea seems to be some pluggable behavior. And as I look at these changes, it seems to me that we’d do better to move them upward in the decision hierarchy, since the decision we’re implementing is a high-level one.

Let’s do one at a time. I’d like to move the attribute sheet drawing up, but it’s called from two places:

function Monster:drawSheet()
    if self:manhattanDistanceFromPlayer() <= 5 then
        self.attributeSheet:draw()
    end
end

function Player:drawExplicit(tiny)
    if tiny and DevMap then return end
    local sx,sy
    local dx = 0
    local dy = 30
    pushMatrix()
    pushStyle()
    spriteMode(CENTER)
    local center = self:graphicCenter() + vec2(dx,dy)
    translate(center.x,center.y)
    if self:isDead() then tint(0)    end
    sx,sy = self:setForMap(tiny)
    self:drawSprite(sx,sy)
    popStyle()
    popMatrix()
    if not tiny then
        self.attributeSheet:draw()
    end
end

The nest gets more complex:

Monster.normalBehavior = {
    query=function(monster) return monster:queryName() end,
    flip=function(monster) monster:flipTowardPlayer() end,
    drawSheet=function(monster) monster:drawSheet() end,
    isActive=function(monster) return monster:isAlive() end,
}

Monster.mimicBehavior = {
    query=function(mimic) 
        if mimic.awake then
            return "HAHAHA!!! I'm a MIMIC!!!"
        else
            return"I am probably a mysterious chest."
        end
    end,
    flip = function(mimic) if mimic.awake then mimic:flipTowardPlayer() end end,
    drawSheet = function(mimic) if mimic.awake then mimic:drawSheet() end end,
    isActive = function(mimic) return mimic.awake and mimic:isAlive() end,
}

function Monster:drawExplicit()
    local r,g,b,a = tint()
    if r==0 and g==0 and b==0 then return end
    self:drawMonster()
    self.behavior.drawSheet(self)
end

That last bit is particularly weird, displaying only the sheets of monsters who aren’t tinted. That’s old code: we used to tint them green when they were dead.

We could certainly put a more fundamental sheet-drawing method in the common superclass of Player and Monster, Entity, and then plug it there. We’d enjoy that particularly, because of folks telling us not to put behavior in superclasses.

I think I want to try something easier. Let’s do this one:

function GameRunner:scaleForLocalMap()
    if DevMap then
        self:getDungeon():setVisibility()
        translate(0,0)
        local hs = WIDTH/(self.tileSize*(1+self.tileCountX))
        local vs = HEIGHT/(self.tileSize*(1+self.tileCountY))
        local s = math.min(vs,hs)
        scale(s)
    else
        local center = self.player:graphicCorner()
        translate(WIDTH/2-center.x, HEIGHT/2-center.y)
        scale(1)
    end
end

This code needed an extract method anyway:

function GameRunner:scaleForLocalMap()
    if DevMap then
        self:scaleForDevMap()
    else
        self:scaleForPlayerMap()
    end
end

function GameRunner:scaleForDevMap()
    self:getDungeon():setVisibility()
    translate(0,0)
    local hs = WIDTH/(self.tileSize*(1+self.tileCountX))
    local vs = HEIGHT/(self.tileSize*(1+self.tileCountY))
    local s = math.min(vs,hs)
    scale(s)
end

function GameRunner:scaleForPlayerMap()
    local center = self.player:graphicCorner()
    translate(WIDTH/2-center.x, HEIGHT/2-center.y)
    scale(1)
end

Now we could plug one or the other of these into a GameRunner member:

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:createNewDungeon()
    self.cofloater = Floater(50,25,4)
    self.musicPlayer = MonsterPlayer(self)
    self:initializeSprites()
    self.dungeonLevel = 0
    self.requestNewLevel = false
    self.playerRoom = 1
    self.mapScaler = "scaleForPlayerMap"
    Bus:subscribe(self, self.createNewLevel, "createNewLevel")
end

function GameRunner:scaleForLocalMap()
    self[self.mapScaler](self)
end

This should work now for the player map, and the dev map will be broken as to scaling. And that’s what happens. Now let’s extend the code for the parameter a bit:

    parameter.boolean("DevMap", false, function()
        if DevMap then
            Runner.mapScaler = "scaleForDevMap"
        else
            Runner.mapScaler = "scaleForPlayerMap"
        end
    end)

This is wicked, but it might well work. And in fact it does.

We’ve replaced one of the if statements with a pluggable method. Of course we have a new if statement in the parameter, but now that is easily replaced:

    local scaleOptions = {}
    scaleOptions[false] = "scaleForPlayerMap"
    scaleOptions[true] = "scaleForDevMap"
    parameter.boolean("DevMap", false, function()
        Runner.mapScaler = scaleOptions[DevMap]
    end)

This works. I’ll explain it in a moment, but it’s supper time.

I think I’ll commit this. We can back it out if we need to, but it’s a lot like I had in mind.

Commit: map scaling is pluggable based on DevMap parameter.

After Supper

Right, I don’t like that well enough. Let’s try this another way. How about the EventBus?

    ...
    local modeOptions = {}
    modeOptions[false] = "playerMode"
    modeOptions[true] = "devMode"
    parameter.boolean("DevMode", false, function(mode)
        Bus:publish(modeOptions[mode])
    end)
    ...

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:createNewDungeon()
    self.cofloater = Floater(50,25,4)
    self.musicPlayer = MonsterPlayer(self)
    self:initializeSprites()
    self.dungeonLevel = 0
    self.requestNewLevel = false
    self.playerRoom = 1
    self:setPlayerMode()
    Bus:subscribe(self, self.createNewLevel, "createNewLevel")
    Bus:subscribe(self, self.setDevMode, "devMode")
    Bus:subscribe(self, self.setPlayerMode, "playerMode")
end

function GameRunner:setDevMode()
    self.mapScaler = self.scaleForDevMap
end

function GameRunner:setPlayerMode()
    self.mapScaler = self.scaleForPlayerMap
end

function GameRunner:scaleForLocalMap()
    self:mapScaler()
end

That works just fine, and it keeps knowledge of what’s to be done in the object that knows. Now we can find the other users of DevMap and fix them up. First we’ll change them to refer to DevMode.

function GameRunner:drawTinyMapOnTopOfLargeMap()
    if DevMode then return end
    pushMatrix()
    self:scaleForTinyMap()
    Runner:drawMap(true)
    popMatrix()
end

function Player:drawExplicit(tiny)
    if tiny and DevMap then return end
    ...

function AttributeSheet:draw()
    if DevMap then return end
    ...

Now we’re fully set up. Let’s improve the others, if improvement it is, the same way.

function AttributeSheet:init(monster, inward)
    self.inward = inward or 0
    self.monster = monster
    self.healthIcon = AdjustedSprite(asset.builtin.Small_World.Heart)
    self.speedIcon = AdjustedSprite(asset.builtin.Space_Art.Green_Explosion, vec2(0.4,0.4))
    self.strengthIcon = AdjustedSprite(asset.builtin.Small_World.Sword, vec2(0.8,0.8), 45)
    self.keyIcon = AdjustedSprite(asset.builtin.Planet_Cute.Key, vec2(30/100,30/100))
    self:doDraw()
    if Bus then
        Bus:subscribe(self, self.doDraw, "playerMode")
        Bus:subscribe(self, self.dontDraw, "devMode")
    end
end

function AttributeSheet:doDraw()
    self.drawSheet = self.doTheDraw
end

function AttributeSheet:dontDraw()
    self.drawSheet = self.doNothing
end

function AttributeSheet:draw()
    self:drawSheet()
end

function AttributeSheet:doNothing()
end

function AttributeSheet:doTheDraw()
    local m = self.monster
    if not m:displaySheet() then return end
    pushMatrix()
    pushStyle()

That’s a lot of darn code to replace one if statement. And it’s far less clear.

This isn’t a good idea for AttributeSheet and it’s not a great idea for the scaling, either.

I’m going to revert this and pull out the change I did before supper. Done. We’re back as of this morning, using DevMap as a flag in a few different spots.

Maybe I’ll have a better idea tomorrow. Or maybe I’ll move on to something useful. Conceivably both but preferably the latter if I can.

And to all a good night!