Last time we closed with:

Next time we’ll play the game a bit and tune these. We have the odd look of Ship:move() to worry about, and the fact that Main.setup() looks confusing, and the necessity to remember to swipe and save new parameters. All those are for next time, and times after next time.

I guess we’d better do that. Having played a while, we settled on these settings:

  • VMax 5
  • Turn 2
  • Thrust 0.02

We think we’ll adjust our parameters to make these values the center of a tunable region of values, for finer-grade tuning. We also noticed:

  • Missiles kill each other, making it impossible to just hold down the fire button;
  • It’s hard to tell the ships apart, leading to confusion as you try to drive the wrong one;
  • We need to display the ship’s remaining hit count and some indication of a hit;
  • When thrusting, fire should shoot out of your aft area;
  • Game should restart automatically some interval after one person dies.

As we set out to adjust the parameter ranges, we realize that we don’t have a good sense of historical values. We wrote down that we stopped VMax at 5, but we don’t recall where it started. We’re pretty sure we only lowered it, for values of pretty sure. We think VMax may have started, today, at 10. We only have one saved state and we have no way to see it other than by restarting the game. Tozier suggests that we could have some kind of display of current vs saved setting on the screen. We could perhaps even have it in the parameter area if we read it into two variables. We’re apparently deciding to defer this question for the moment.

Looking at the parameter code, we’re reminded that the structure of the function is questionable:

function setup()
    displayMode(STANDARD)
    parameter.action("Automate", function()
        automate()
    end)
    parameter.action("Run", function()
        newGame()
    end)
    parameter.integer("VMax", 2, 20)
    parameter.integer("Turn", 1, 12)
    parameter.number("ThrustAmount", 0.01, 0.20)
    parameter.action("Save", function()
        saveLocalData("VMax", VMax)
        saveLocalData("Turn", Turn)
        saveLocalData("ThrustAmount", ThrustAmount)
    end)
    VMax = readLocalData("VMax")
    Turn = readLocalData("Turn")
    ThrustAmount = readLocalData("ThrustAmount")
end

Should we refactor first, or just adjust the parameters? Let’s refactor, it’s the right thing to do: we’ve complained about this code before.

There are two basic bits in there, three if you count setting the display mode. The remaining two(!) are define the parameters, and set the parameters from their saved values. The action parameter looks confusing:

    parameter.action("Save", function()
        saveLocalData("VMax", VMax)
        saveLocalData("Turn", Turn)
        saveLocalData("ThrustAmount", ThrustAmount)
    end)

All this does is define the button but it looks like it’s doing something. We think we’ll extract that inner function and refer to it. Here goes:

function setup()
    displayMode(STANDARD)
    parameter.action("Automate", function()
        automate()
    end)
    parameter.action("Run", function()
        newGame()
    end)
    parameter.integer("VMax", 2, 20)
    parameter.integer("Turn", 1, 12)
    parameter.number("ThrustAmount", 0.01, 0.20)
    parameter.action("Save", saveParameters)
    VMax = readLocalData("VMax")
    Turn = readLocalData("Turn")
    ThrustAmount = readLocalData("ThrustAmount")
end

function saveParameters()
    saveLocalData("VMax", VMax)
    saveLocalData("Turn", Turn)
    saveLocalData("ThrustAmount", ThrustAmount)
end

This seems to work. Now let’s refactor out the parameter setting, and then the reading:

function setup()
    displayMode(STANDARD)
    defineParameters()
    VMax = readLocalData("VMax")
    Turn = readLocalData("Turn")
    ThrustAmount = readLocalData("ThrustAmount")
end

function defineParameters()
    parameter.action("Automate", function()
        automate()
    end)
    parameter.action("Run", function()
        newGame()
    end)
    parameter.integer("VMax", 2, 20)
    parameter.integer("Turn", 1, 12)
    parameter.number("ThrustAmount", 0.01, 0.20)
    parameter.action("Save", saveParameters)
end

Now the defines are removed. We notice the in-line functions inside defineParameters(). I’m inclined to leave one-liners in line. Now let’s refactor out the reading:

function setup()
    displayMode(STANDARD)
    defineParameters()
    readParameters()
end
function readParameters()
    VMax = readLocalData("VMax")
    Turn = readLocalData("Turn")
    ThrustAmount = readLocalData("ThrustAmount")
end

Tozier points out that the word “Parameters” is perhaps confusing, since these are really buttons or controls or some such thing, which happen to be implemented with the Codea parameter statement. We’re not troubled enough to change it. What are those things? Controllable globals? Switches? We don’t know and for now, don’t care. OH: “Widgets?”

Our tests mostly run (we have not yet fixed the Button tests, which depend on the old button positions) so we’ll commit the code. Maybe we should prioritize fixing those tests, however. In addition, our “Automate” button, which was used to quickly fire a couple of missiles and kill both ships, is no longer useful. I’m inclined to remove automate and bump the priority of fixing the button tests.

At a quick glance and two tries we observe that our test buttons didn’t define our new width and height member variables. Defining them fixes the tests. Whee. One line change not worth looking at but here goes:

        _:before(function()
            dSave = U.Drawn
            tSave = U.Touched
            U.Drawn = {}
            U.Touched = {}
            button = Button(100, 100, 50, 50) -- used to be (100,100)
        end)

OK, tests are green, commit.

We haven’t actually adjusted the parameter ranges yet. We could do that. On the other hand I’m hungering to fix the missiles killing themselves. Product Owner demands a new feature and the team buckles under the pressure. I hate when that happens.

I suggest that one “easy” fix is to change missiles so that the missiles from a given ship will not kill each other, but will still kill missiles from the other ship. T points out that it’d be even easier to make missiles not kill missiles. R objects that shooting down the other player’s missiles is really cool. T now objects courteously that the missiles should be colored or otherwise matched to the ship that owned them. R assumes but does not mention that he suspects T is worried about wasting missiles on missiles that couldn’t kill him.

If ships had colors, then missiles could easily pick up those colors. R votes to defer the color issue for the moment.

function Missile:init(ship, testVel, startPos)
    local myVel = testVel or vec2(0,1)
    self.ship = ship
    self.name = "missile " .. Missile.count
    self.damageDealt = 1
    self.hitpoints = 1
    Missile.count = Missile.count + 1
    self.pos = ship.pos + (startPos or self:aWaysOutInFront(ship))
    local velocity = myVel:rotate(math.rad(ship.heading))
    self.vel = ship.vel + velocity
    self.maxDistance = WIDTH*1.4
    self.distance = 0
    U:addObject(self)
end

function Missile:hitBy(anObject)
    if anObject.ship == self.ship then return end
    self.hitpoints = self.hitpoints - (anObject.damageDealt or 0)
    if self.hitpoints <= 0 then 
        self:die()
    end
end

We linked each missile back to its ship in init() and checked to see if the object hitting us has the same ship as we do. If it has no ship, it certainly won’t equal ours. We test this manually and it works as intended.

Should we have an automated test for things like this? Trick question, of course we should. How could we have such a test? This is a bit more difficult. Will it pay off to have the test? When we don’t know, we try to err on the side of writing the test. We don’t have any automation left, having removed the button. We could put it back and devise some clever object creation and testing behavior. Or we could just create a couple of missiles, ships, whatever, and send them messages. (This is what T was trying to get at when I asked the question.)

Heck, let’s try it.

Wow, sometimes doing the moral thing pays off. We have a testKill test, which we enhance as follows:

        _:test("Missiles from separate ships kill each other", function()
            local ship1 = Ship(1)
            local ship2 = Ship(2)
            local missile1 = Missile(ship1)
            local missile2 = Missile(ship2)
            missile1.pos = vec2(100,100)
            missile2.pos = vec2(100,100)
            U:updateUniverse()
            U:interactAll()
            local dead = {}
            for _, obj in pairs(U.Removes) do
                dead[obj] = obj.name
            end
            _:expect(dead).has("missile 1")
            _:expect(dead).has("missile 2")
        end)
                
        _:test("Missiles from same ships don't kill each other", function()
            local ship1 = Ship(1)
            local missile1 = Missile(ship1)
            local missile2 = Missile(ship1)
            missile1.pos = vec2(100,100)
            missile2.pos = vec2(100,100)
            U:updateUniverse()
            U:interactAll()
            local dead = {}
            for _, obj in pairs(U.Removes) do
                dead[obj] = obj.name
            end
            _:expect(dead).hasnt("missile 1")
            _:expect(dead).hasnt("missile 2")
        end)

What we did there was create a ship or two, have each one fire a missile, place the missiles at the same location, interact all the objects, and check to see whether the missiles are scheduled to be Removed. This uses those convention has/hasn’t functions.

We note an opportunity for some refactoring here but we are leaving that for another day. We have also not even changed those parameters yet, though we have done some excellent refactoring. Anyway, first commit these new tests.

Now, we adjust the parameter statements:

function defineParameters()
    parameter.action("Run", function()
        newGame()
    end)
    parameter.number("VMax", 2, 8)
    parameter.number("Turn", 1, 3)
    parameter.number("ThrustAmount", 0.00, 0.05)
    parameter.action("Save", saveParameters)
end

Note that we changed VMax and Turn to be number rather than integer. That works but we note that the slider is finicky. It looks like the number parameter rounds to two decimal places, which is almost too granular for thrust and definitely too fiddly for VMax and Turn. But good enough to play the game and make some refined decisions.

We’ll report back on that next time.