Maybe we need a bit of console display. And I’m sure we’ll find some code to improve. Foraging, basically.

Now that a move can be stopped by trying to go through an Obstacle, or by falling into a Pit, we might want to have some indication of the problem fed back to the player. I’m envisioning a simple text display of the robot’s status.

That will give me an “opportunity” to improve the rather ad-hoc display code we tweaked yesterday, so that the robot, when in a pit, appears with the black pit background under it.

The screen looks like this, with the console showing.

robot screen, robot is in a pit

There are surely other matters to deal with in the display, including the possibility of going off the edge of the world. I think the current World is providing an essentially infinite space in which to be. The “spec” says that you can wind up with a status of “Edge”. Apparently you can’t fall off.

Let’s do some display. Unlike most code, I rarely test-drive display code, because it’s generally difficult and my tests have no sense of style. If some aspect is complicated, I might build supporting objects test-driven.

Current Display

function drawGame()
    setUpDrawing()
    drawScreen()
end

function setUpDrawing()
    background(40,40,40)
    rectMode(CENTER)
    textMode(CENTER)
    translate(WIDTH/2, HEIGHT/2)
    stroke(0,256,0,96)
    strokeWidth(2)
    fontSize(30)
end

function drawScreen()
    local size = 40
    noFill()
    for x = -10,10 do
        for y = -10,10 do
            drawCell(x,y, size)
        end
    end
end

function drawCell(x,y, size)
    local fact = CurrentRobot:factAt(x,y)
    local gd = GridDisplay:display(fact)
    if gd.color then
        fill(gd.color)
    else
        noFill()
    end
    rect(x*size,y*size,size,size)
    if gd.text then
        fill(0,256,0)
        text(gd.text, x*size,y*size)
    end
    if gd.asset then
        pushMatrix()
        scale(0.8, 0.8)
        sprite(gd.asset,x,y)
        popMatrix()
    end
    if x == 0 and y == 0 then
        dir = CurrentRobot:direction()
        fact = "R"..dir
        gd = GridDisplay:display(fact)
        pushMatrix()
        scale(0.8, 0.8)
        sprite(gd.asset,x,y)
        popMatrix()
    end
end

This is pretty straightforward, really. We draw ten cells left, right, up, and down from the robot. The cells are drawn based on the GridDisplay object, which we may look at later. There is a special deal for the display of the robot, there at the end. This change was made so that its icon would display on top of the black square that represents a pit.

Let’s refactor a bit. There is really no point to testing each cell to see if it is the robot: we know that it is drawn at the (0,0) point of the display (which is centered at screen center via the translate).

First this:

    if x == 0 and y == 0 then
        dir = CurrentRobot:direction()
        fact = "R"..dir
        gd = GridDisplay:display(fact)
        pushMatrix()
        scale(0.8, 0.8)
        sprite(gd.asset, 0, 0)
        popMatrix()
    end

Here I just changed the drawing of the sprite to always be at 0,0. Should have no effect. Has no effect.

Now extract function on the insides of the if statement:

    if x == 0 and y == 0 then
        drawRobot()
    end
end

function drawRobot()
    dir = CurrentRobot:direction()
    fact = "R"..dir
    gd = GridDisplay:display(fact)
    pushMatrix()
    scale(0.8, 0.8)
    sprite(gd.asset, 0, 0)
    popMatrix()
end

Again no visible effect. Now call the function only once, after drawing everything else.

function drawGame()
    setUpDrawing()
    drawScreen()
    drawRobot()
end
...

function drawCell(x,y, size)
    local fact = CurrentRobot:factAt(x,y)
    local gd = GridDisplay:display(fact)
    if gd.color then
        fill(gd.color)
    else
        noFill()
    end
    rect(x*size,y*size,size,size)
    if gd.text then
        fill(0,256,0)
        text(gd.text, x*size,y*size)
    end
    if gd.asset then
        pushMatrix()
        scale(0.8, 0.8)
        sprite(gd.asset,x,y)
        popMatrix()
    end
end

I removed the if and its call, and added the call in drawGame. No visible effect. Let’s commit, this is a nice save point. Commit: refactoring drawing.

I think the function drawScreen should be renamed to drawMap, since the screen is kind of the whole thing. Rename. All good. Commit: rename drawScreen to drawMap.

OK, now to draw the robot’s HUD, the head’s-up display of info that we might need to know. I plan to display the status information we need. Ultimately I think that should include the result of the most recent command, including the message, and the robot’s state, which is always returned, showing position, direction, number of hits, number of shots, and status. We don’t currently support all that information, at least not in detail.

We’ll start with a simple bit of text off to the right of the map.

function drawGame()
    setUpDrawing()
    drawMap()
    drawRobot()
    drawHUD()
end

And, just to see where we might want to draw, I do a bit of ad-hoc experimentation, winding up with this code, full of magic numbers:

function drawHUD()
    pushMatrix()
    pushStyle()
    fill(256)
    translate(WIDTH*0.4,0)
    t = {"HUD", "OK", "(25,25)", "Obstacle", "Hits: 99", "Shots: 99"}
    for y = 1,5 do
        text(t[y], 0, -y*40+120)
    end
    popStyle()
    popMatrix()
end

That gives me this display:

HUD display off to right of map

The worst aspect of this, which isn’t visible to the reader, is that the translate of 0.4*WIDTH is relative to the initial translation to WIDTH/2,HEIGHT/2. We should make those independent. I’ll do that by moving the first translation into drawMap:

function setUpDrawing()
    background(40,40,40)
    rectMode(CENTER)
    textMode(CENTER)
    stroke(0,256,0,96)
    strokeWidth(2)
    fontSize(30)
end

function drawMap()
    pushMatrix()
    translate(WIDTH/2, HEIGHT/2)
    local size = 40
    noFill()
    for x = -10,10 do
        for y = -10,10 do
            drawCell(x,y, size)
        end
    end
    popMatrix()
end

That will move the HUD over to the left somewhere. Test to see:

HUD left and down

Correct. Left and down. Now, though, we can decide where we actually want it. I’ll start with centering it in Y and at 3/4 width:

function drawHUD()
    pushMatrix()
    pushStyle()
    fill(256)
    translate(WIDTH*0.75,HEIGHT/2)
    t = {"HUD", "OK", "(25,25)", "Obstacle", "Hits: 99", "Shots: 99"}
    for y = 1,5 do
        text(t[y], 0, -y*40+120)
    end
    popStyle()
    popMatrix()
end

HUD toward right in map

That looks nice, but we can’t really overlay the map. Probably 7/8 was about right:

    translate(7*WIDTH/8,HEIGHT/2)

I forgot to take a picture, but I think that’s about right. Now let’s look at what’s going on in the loop: I actually had a bit of intention in there:

function drawHUD()
    pushMatrix()
    pushStyle()
    fill(256)
    translate(7*WIDTH/8,HEIGHT/2)
    t = {"HUD", "OK", "(25,25)", "Obstacle", "Hits: 99", "Shots: 99"}
    for y = 1,5 do
        text(t[y], 0, -y*40+120)
    end
    popStyle()
    popMatrix()
end

The main thing in there is the -y*40, which is spacing each line 40 pixels from the next. I did that because the squares on the map are 40 pixels, so the HUD lines line up nicely with the map. The +120 is there to adjust the height, and the - is because y increases upward in Codea, not downward.

This experiment gives me the necessary magic numbers to get the look that I want. Now we really need to display the desired information. That’s all taken from the most recent response from the world. The Robot has that.

Now if we were working with JavaFX or some other graphical framework, we might have a standard way of doing this, a ResponseView or something. We do not have that kind of framework, and my practice is to discover the objects that I need as I go. My starting plan is that in the drawHUD we’ll tell the Robot to draw the HUD and it will tell the Response. Like this:

function drawHUD()
    pushMatrix()
    pushStyle()
    fill(256)
    translate(7*WIDTH/8,HEIGHT/2)
    CurrentRobot:drawHUD()
    popStyle()
    popMatrix()
end

function Robot:drawHUD()
    self._response:drawHUD()
end

function Response:drawHUD()
    text("HELLO", 0,0)
end

Test this. I think it might explode, because I’m not sure we have a response at the very beginning. We’ll find out.

No, works as intended:

HELLO appears in HUD position

Now we just have to display a bit of stuff.

I’ve broken something. The map no longer fills in. I think that moving the translate broke it. Ah. Yes, the drawRobot needs the translation. Move it from the top level, inside drawMap:

function drawMap()
    pushMatrix()
    translate(WIDTH/2, HEIGHT/2)
    local size = 40
    noFill()
    for x = -10,10 do
        for y = -10,10 do
            drawCell(x,y, size)
        end
    end
    drawRobot()
    popMatrix()
end

Back to the HUD, I’ve got this:

function Response:drawHUD()
    local y = 120
    text(self:result(),0,y)
    local pos = "("..self:x()..","..self:y()..")"
    text(pos,0,y-40)
end

OK and position

So far so good. Moving right along:

function Response:drawHUD()
    local y = 120
    text(self:result(),0,y)
    local pos = "("..self:x()..","..self:y()..")"
    text(pos,0,y-40)
    text(self:direction(),0,y-80)
    text(self:message(), 0, y-120)
end

This works nicely, with one tiny exception. We only see the Pit message when we try to move out of the pit, not when we first hit it. Watch:

hud moving showing Obstacle and Pit message

I think we need to check Pit status in World, on exit from the motion routine. First let’s commit the HUD: Initial HUD commit.

Fix the Pit Problem

We digress to fix the Pit display:

function World:moveToLimit(robot, xStep, yStep, distance)
    local new_x, new_y = robot._x, robot._y
    for i = 1,distance do
        if self:factAt(new_x, new_y) == "PIT" then
            return "Pit"
        end
        new_x = new_x + xStep
        new_y = new_y + yStep
        if self:factAt(new_x, new_y) == "OBSTACLE" then
            return "Obstacle"
        end
        robot._x = new_x
        robot._y = new_y
        if self:factAt(new_x, new_y) == "PIT" then
            return "Pit"
        end
    end
    return ""
end

This adds some duplication, but we need to avoid moving each time we enter, and to return the info even on the first fall. We’ll test this and leave it, given that it works. It does. Commit: Pit message now returns on first entry to a pit.

I do think this code is messy enough that we should consider improving it, but it’s all isolated into that one method, so we’ll let it ride for now. We’ll be back.

What else should the HUD display? Hits and Shots, according to the spec, I guess.

I think it’s time to improve the method as well:

function Response:drawHUD()
    local y = 120
    text(self:result(),0,y)
    local pos = "("..self:x()..","..self:y()..")"
    text(pos,0,y-40)
    text(self:direction(),0,y-80)
    text(self:message(), 0, y-120)
end

We’re subtracting 40s from y as we display. Surely the computer could help with that. I have an idea:

function Response:drawHUD()
    local y = 120
    local displayLine = function(line)
        text(line,0,y)
        y = y - 40
    end
    displayLine(self:result())
    local pos = "("..self:x()..","..self:y()..")"
    displayLine(pos)
    displayLine(self:direction())
    displayLine(self:message())
end

That works perfectly, the display is as before:

still good display

Hits and Shots should go before the message, which is optional. Otherwise we’d have a blank line.

I don’t think we even support those values yet. Ah, but we do …

function World:robotState(robot)
    return {
        position = {robot._x, robot._y},
        direction = robot._direction,
        status = "NORMAL",
        shots = robot._shots,
        shields = robot._strength
    }
end

Do we interpret those in Response? If not, we certainly can do.

function Response:drawHUD()
    local y = 120
    local displayLine = function(line)
        text(line,0,y)
        y = y - 40
    end
    displayLine(self:result())
    local pos = "("..self:x()..","..self:y()..")"
    displayLine(pos)
    displayLine(self:direction())
    displayLine("Hits: "..self:hits())
    displayLine(self:message())
end

function Response:hits()
    return self._r.state.hits or "???"
end

I had to add the or "???" because I didn’t find any hits in the _r dictionary. I have to figure out who’s doing what. It seems likely that data isn’t coming back, or is coming back as nil. Or it could be that the variable is called shields, not hits. 😀

Changing that works just fine. Carrying on to Shots, and after adjusting y for appearance, we have this:

function Response:drawHUD()
    local y = 80
    local displayLine = function(line)
        text(line,0,y)
        y = y - 40
    end
    displayLine(self:result())
    local pos = "("..self:x()..","..self:y()..")"
    displayLine(pos)
    displayLine(self:direction())
    displayLine("Shots: "..self:shots())
    displayLine("Shields: "..self:shields())
    displayLine(self:message())
end

And in play it looks like this:

final hud looking good

Commit: HUD displays status, position, direction, shots, shields, and message.

I’m calling that good. Let’s sum up.

Summary

We started with a nice little bit of refactoring to pull out the drawing of the robot, so that we could call it once and avoid that odd check for x and y equaling zero. (And later, I cleverly made a change to where I did the translate that broke that refactoring. Quickly found and fixed.)

The biggest glitch was a discovery, that the Pit message didn’t come out on moving into the pit, only on trying to move past it. Now that I think of it, that’s a gap in the testing, because in the only Pit motion test I tried to move past it, not just into it. Anyway it’s fixed, but we should probably add some tests there.

I do think we’ll be back to that code, as we have other things in the world to encounter.

The HUD display code went smoothly as well. Starting with ad hoc numbers and then refactoring to use them more sensibly has resulted in a mostly good situation. We do have the magic number “40” floating about, and the “80” and “40” in the HUD display relate to that number. We really ought to pull out the magical display numbers and, for that matter probably some other values such as colors. We’ll keep that in mind but today’s work has left us in better shape than we were, so I don’t think we’ll put in overtime to figure out what to do with our magic numbers.

The choice to have Response draw itself as a HUD is typical of my Codea style. As I mentioned, other drawing schemes would have us creating a ResponseView or something, but at this point I don’t see the value.

Mixing functions and methods

This code does something worth commenting about:

function Response:drawHUD()
    local y = 80
    local displayLine = function(line)
        text(line,0,y)
        y = y - 40
    end
    displayLine(self:result())
    local pos = "("..self:x()..","..self:y()..")"
    displayLine(pos)
    displayLine(self:direction())
    displayLine("Shots: "..self:shots())
    displayLine("Shields: "..self:shields())
    displayLine(self:message())
end

That little function displayLine references the local value y and decrements it. In Lua, functions are often used this way, but in an OO situation without first-class functions, we might have created a tiny object with a y value and a displayLine method, encapsulating the same idea, drawing subsequent lines at lower and lower positions. We might even have given our object a step size as well as an initial value.

I think this way is more compact and therefore more understandable. Your view might differ, and you might prefer some other approach. If you have one you’d like my comments on, or just to show me, tweet me up.

Overall, in a couple of hours, we’ve implemented a neat little HUD and fixed a discovered problem with falling into a Pit.

We do owe ourselves some new tests.

A good morning’s work. See you next time!