I have yet another idea about how to handle switching between developer and player modes. This might be what Bruce Onder was talking about.

I want to begin by making a table of all the things we currently do differently in Dev and Player modes.

Object Behavior Player Dev
GameRunner draw tiny map draw no
GameRunner scale large map 1 ~= 0.25
Player draw explicit yes no if tiny
AttributeSheet draw yes no


I’ll prepare the ground a bit by breaking out those two scaling methods, as I did before but reverted:

function GameRunner:scaleForLocalMap()
    if DevMap then
        self:scaleForDeveloper()
    else
        self:scaleForPlayer()
    end
end

function GameRunner:scaleForDeveloper()
    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:scaleForPlayer()
    local center = self.player:graphicCorner()
    translate(WIDTH/2-center.x, HEIGHT/2-center.y)
    scale(1)
end

My coding standards call for that extraction anyway, but I think we’ll find it useful if my cunning plan works at all.

Commit: refactor GameRunner:scaleForLocalMap

What is your cunning plan, anyway?

I propose to build two strategy objects, one representing player mode and one representing developer mode. In every instance where we want a difference between dev and player mode, the relevant object will send a message to the currently-live strategy object, which will dispatch back to do the right thing.

This is almost certainly what Bruce Onder was talking about in his tweet:

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?

If it’s not exactly what he meant, it certainly rhymes with it. So all credit to him in what follows. Except for the part about actually doing it.

I think this could be done with an array of functions, and that might save us a couple of classes. But I like classes. Let’s stick with the OO style. Maybe someday we’ll do another project that’s all functions. (But probably not.)

New tab, OperatingModes. I type in two little classes:

PlayerMode = class()

function PlayerMode:scaleForLocalMap(sender)
    sender:scaleForPlayer()
end

DevMode = class()

function DevMode:scaleForLocalMap(sender)
    sender:scaleForDeveloper()
end

Now I think I want to make a well-known object, at least to start, and to set it when the mode switches.

    OperatingMode = PlayerMode()
    parameter.boolean("ShowMap",false)
    parameter.boolean("DevMap", false, function(mode)
        if mode then 
            OperatingMode = DevMode()
        else 
            OperatingMode = PlayerMode()
        end
    end)

Now I should be able to do this:

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

switch

So that works. Life is good. Next let’s do this one:

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

We have that long name to remind us that it has to be drawn late in the process. We’ll keep that but we need to extract the works here.

function GameRunner:drawTinyMapOnTopOfLargeMap()
    OperatingMode:drawTinyMap(self)
end

function GameRunner:drawTinyMap()
    pushMatrix()
    self:scaleForTinyMap()
    Runner:drawMap(true)
    popMatrix()
end

And:

function PlayerMode:drawTinyMap(sender)
    sender:drawTinyMap()
end

function DevMode:drawTinyMap()
end

This should be fine. And it is. Next:

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

OK, I’m going by rote here but this one is a bit tricky. Extract the guts as always:

function Player:drawExplicit(tiny)
    OperatingMode:drawInLargeAndSmallScale(tiny,self)
end

function Player:drawInLargeAndSmallScale(tiny)
    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

And:

function PlayerMode:drawInLargeAndSmallScale(tiny, sender)
    sender:drawInLargeAndSmallScale(tiny)
end

function DevMode:drawInLargeAndSmallScale(tiny, sender)
    if not tiny then
        sender:drawInLargeAndSmallScale(tiny)
    end
end

Again, it works just fine. I’m not fond of having to check the tiny flag, but we need to do it unless we change how the Player draws herself twice.

One more:

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

Same drill:

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

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

And:

function PlayerMode:drawSheet(sender)
    sender:drawSheet()
end

function DevMode:drawSheet()
    -- no action
end

I decided to add a comment to the empty ones, and was able to resist “this method intentionally blank”.

I expect this to work. And it does:

sheets vanish

There is one odd effect, which is that the edge tiles, which we normally do not see, show up after we leave dev mode. We could fix that, but I don’t think it’s worth it. The small map is entirely filled in after leaving dev mode as well, because since the whole dungeon has been seen, all the rooms show on the small map. Those are the rules.

I think this is good. Commit: OperatingMode, PlayerMode, DevMode handling all switching between modes.

Summary

Well, that went smoothly, didn’t it? The need to pass the tiny flag is a bit naff, but at this moment I don’t see a good way around it. And it’s somewhat irritating having a global name for OperatingMode, but you have to admit that it makes for a rather clean interface. We could hide it in Runner, but the path from AttributeSheet to the runner is at least two rails.

I am perilously close to allowing everyone to access the Runner as a global. I think it might actually make the code easier to understand.

Nonetheless, globals are always worth some concern, as they tend to make changes difficult. We’ll see whether this one bugs us.

So what are these DevMode PlayerMode things? They’re built as class instances, but they don’t really have methods of their own, they just forward a message back to the sender, or do nothing. In essence, this is the Strategy Pattern in operation, and it seems to have served nicely.

The very nice thing about this implementation is that it seems just right for the current situation: four cases, four tiny methods. However, it should serve nicely for any additional switching we may need to do for developer convenience.

There are two things we might do now. One is that the ShowMap toggle is just about wasted, since going into Dev Mode and out will display the small map. So let’s remove that. We delete:

    parameter.boolean("ShowMap",false)

And this:

function Tile:drawMapCell(center)
    if self.kind == TileRoom and (ShowMap or self.seen) then
        pushMatrix()
        pushStyle()
        rectMode(CENTER)
        translate(center.x, center.y)
        fill(255)
        rect(0,0,TileSize,TileSize)
        popStyle()
        popMatrix()
    end
end

Becomes this:

function Tile:drawMapCell(center)
    if self.kind == TileRoom and self.seen then
        pushMatrix()
        pushStyle()
        rectMode(CENTER)
        translate(center.x, center.y)
        fill(255)
        rect(0,0,TileSize,TileSize)
        popStyle()
        popMatrix()
    end
end

Test and commit: Remove ShowMap feature.

The second is that we could remove the if statement from this:

    parameter.boolean("DevMap", false, function(mode)
        if mode then 
            OperatingMode = DevMode()
        else 
            OperatingMode = PlayerMode()
        end
    end)

How? Well, we could make a table indexed by true/false, containing class names, and instantiate the appropriate one. That would take more lines of code and be less clear. There is a time and a place for a conditional, and for now, I’m happy with this one.

So. Good-looking solution. Thanks to Bruce for the idea, which may or may not be the one he intended. All mistakes, of course, are Chet’s.


D2.zip