Some more controls

Last time we added a control limiting ship top speed, one of a few we need for tuning game playability. We encountered a bit of roughness in the code. The issues include:

  • We have a local storage object, which amounts to a table we can save values in and get them back. Tozier thinks of it as a toaster note. No, wait, poster note. Anyway …
  • We have defined some values “at compile time”, as statements in classes but outside functions. These execute when the program is first run.
  • There are some initialization times. There is Main.setup function that runs once when the program starts. Pressing the reset button on the Codea console runs this. I do not know whether it reloads the compile time values, but I doubt it. It would be easy to check …
  • There is a “Run” button among our console buttons. That calls the Main.newGame() function, which you would think would start a new game but see below.
  • It is possible, in principle, that the game control sliders and buttons could remain on screen, although we set everything off-screen with Run. See below again.

Our big but quick discovery was that we didn’t like where the screen setting and the universe init and the newGame were all spread all over. We have moved them all together:

function newGame()
    displayMode(FULLSCREEN_NO_BUTTONS)
    U:init()
    U.sun = Sun()
    Ship1 = Ship(1)
    Ship(2)
end

(You’re probably wondering why we are saving Ship1. The answer is in our cute little quick-kill automate function:

function automate()
    Missile(Ship1, vec2(0,10))
    Ship1.heading = Ship1.heading + 90
    Missile(Ship1, vec2(0,10))
end

We just needed a name for it so we could make it fire missiles. I think we should make it a local but right now maybe we’re draining the swamp.

Anyway, now our game control parameters look like this:

function setup()
    displayMode(STANDARD)
    parameter.action("Automate", function()
        automate()
    end)
    parameter.action("Run", function()
        newGame()
    end)
    parameter.integer("VMax", 2, 20)
    parameter.action("Save", function()
        saveLocalData("VMax", VMax)
    end)
    VMax = readLocalData("VMax")
end

We note that this function now has at least two responsibilities: it sets up the parameter things on the screen, and it reads in our (only) saved value. These responsibilities might want to be broken out soon.

The bigger question, and the reason I’m typing here, is that we need to decide when our parameters should take effect. Should they just be accessed at setup, or should they be live all the time? Right now, our only value, VMax, is used like this:

function Ship:adjustedVelocity(vel, heading, accel)
    local proposed = vel + accel:rotate(math.rad(heading))
    proposed = proposed + U:gravity(self)
    self.MaxSpeed = VMax
    return U:limitSpeed(self.MaxSpeed, proposed)
end

We’ve changed U:limitSpeed to accept the object’s maximum speed as a direct parameter. That’s good. But we are still keeping that max speed in the Ship instance and in the Ship class, as well as in the parameter/global VMax. Tozier suggests just removing all the Ship.MaxSpeed references, and I agree … but the Fragment still has the old MaxSpeed functionality, which will result in an odd form of duplication, two same things done two different ways. We’ll ignore that concern for now and address it either when we play with Fragment speed or clean it up in post.1

So we agree to remove all references to the Ship’s MaxSpeed and we do so.

function Ship:adjustedVelocity(vel, heading, accel)
    local proposed = vel + accel:rotate(math.rad(heading))
    proposed = proposed + U:gravity(self)
    return U:limitSpeed(VMax, proposed)
end

Now the topic I’ve been trying to bang on about is when the parameters will be used: will it be at setup or on the fly? The one we have is accessed on the fly and we like it. I propose that we allow the parameters to be accessed at run time, and as convenient. This may encourage us to move some of the class variables out, as we did with Ship.MaxSpeed, or to a new location.

Since we have a save button, we can play until we like it and then save:

IMAGE HERE

At last …

We can do another parameter. We don’t like the turn rate and we don’t have enough thrust.2

Turning first, then thrust.

Ship = class()

    Ship.MaxMissiles = 50
    Ship.TicksBetweenMissiles = U.MissileKillDistance + 1
    Ship.ThrustAmount = vec2(0,0.02)
    Ship.TurnAmount = 1
    Ship.MaxDamage = 5
    
function Ship:move()
    local turn
    local thrust
    
    if     self.controls.left.pressed  and not self.controls.right.pressed then turn =  Ship.TurnAmount
    elseif self.controls.right.pressed and not self.controls.left.pressed  then turn = -Ship.TurnAmount
    else turn = 0 end
    self.heading = self.heading + turn
    
    if self.controls.thrust.pressed then thrust = Ship.ThrustAmount else thrust = vec2(0,0) end
    self.vel = self:adjustedVelocity(self.vel, self.heading, thrust)
    self.pos = U:clip_to_screen(self.pos + self.vel)
    
    if self.controls.fire.pressed then self:fireMissile() end
end

function Ship:adjustedVelocity(vel, heading, accel)
    local proposed = vel + accel:rotate(math.rad(heading))
    proposed = proposed + U:gravity(self)
    return U:limitSpeed(VMax, proposed)
end

Since in adjustedVelocity we convert heading to radians, we know that heading is in degrees, and since we are adding our TurnAmount to it, TurnAmount is also in degrees. One degree per sixtieth of a second makes for six seconds turning clear around. That’s centuries in the life of a spaceship. Let’s try a range of … 1 through 12, which will allow a full turn in half a second, surely too fast.

Therefore, we remove the Ship.TurnAmount, replacing like this:

function Ship:move()
    local turn
    local thrust
    
    if     self.controls.left.pressed  and not self.controls.right.pressed then turn =  Turn
    elseif self.controls.right.pressed and not self.controls.left.pressed  then turn = -Turn
    else turn = 0 end
    self.heading = self.heading + turn
    
    if self.controls.thrust.pressed then thrust = Ship.ThrustAmount else thrust = vec2(0,0) end
    self.vel = self:adjustedVelocity(self.vel, self.heading, thrust)
    self.pos = U:clip_to_screen(self.pos + self.vel)
    
    if self.controls.fire.pressed then self:fireMissile() end
end

Together with the parameter definitions, which are now:

-- Main

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.action("Save", function()
        saveLocalData("VMax", VMax)
        saveLocalData("Turn", Turn)
    end)
    VMax = readLocalData("VMax")
    Turn = readLocalData("Turn")
end

We encountered an interesting situation there. The setup() code is a bit confusing, because the parameter definitions just define buttons: they don’t do anything. The final two lines of code, however, do something: they read the current values of our saved values into the parameters, initializing them to our saved values. Unless there are no saved values! Then, that parameter will be set to nil. The parameter buttons on the screen don’t reflect this problem, but the code will likely blow up. Operationally, what we need to do when we first set up a new one of these parameters is to ensure it has a value in the local store. We can do this by carefully sliding its slider about and then saving, or we could take some simple step to initialize it once and then remove the init code. We don’t have a solution. For now we’re going to try to be careful.

However, note that “we’ll be more careful” has not always worked out in the past as often as we might wish. This is a danger sign and we should write ourselves a note to do something better. We’ll be careful to write a note …

One more thing while we’re thinking about it. Look again at the Ship:move() function:

function Ship:move()
    local turn
    local thrust
    
    if     self.controls.left.pressed  and not self.controls.right.pressed then turn =  Turn
    elseif self.controls.right.pressed and not self.controls.left.pressed  then turn = -Turn
    else turn = 0 end
    self.heading = self.heading + turn
    
    if self.controls.thrust.pressed then thrust = Ship.ThrustAmount else thrust = vec2(0,0) end
    self.vel = self:adjustedVelocity(self.vel, self.heading, thrust)
    self.pos = U:clip_to_screen(self.pos + self.vel)
    
    if self.controls.fire.pressed then self:fireMissile() end
end

This code looks unlike any other code we can remember writing. There’s something weird about it. Another thing to remember. Now we have two things to remember and I can’t recall what the first one was. Anyone got a card or a place to write stuff down?

Bag it, let’s do thrust:

Ship = class()

    Ship.MaxMissiles = 50
    Ship.TicksBetweenMissiles = U.MissileKillDistance + 1
    Ship.ThrustAmount = vec2(0,0.02)
    Ship.MaxDamage = 5

Thrust here is cleverly a vector but in fact it is really just an increment to be applied in the +Y direction of the ship. Because we drew them nose upward to begin with. Since acceleration is added every 1/60 second, it is 1.2 coordinate points (not really pixels because we’re scaled) per second per second. Or something. Anyway, let’s turn it into a simple linear parameter, ranging between, oh, 0.01 to 0.2 for now, probably that’ll be too much.

function Ship:move()
    local turn
    local thrust
    
    if     self.controls.left.pressed  and not self.controls.right.pressed then turn =  Turn
    elseif self.controls.right.pressed and not self.controls.left.pressed  then turn = -Turn
    else turn = 0 end
    self.heading = self.heading + turn
    
    if self.controls.thrust.pressed then thrust = vec2(0, ThrustAmount) else thrust = vec2(0,0) end
    self.vel = self:adjustedVelocity(self.vel, self.heading, thrust)
    self.pos = U:clip_to_screen(self.pos + self.vel)
    
    if self.controls.fire.pressed then self:fireMissile() end
end

Note that we moved the vector creation inside move() and decided to call the parameter ThrustAmount. We implement it thusly:

 -- Main
 
 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

Some quick testing but not playing gives us these starting values:

IMAGE HERE

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. Here’s the code:


--# Fragment
Fragment = class()

Fragment.MaxSpeed = 8

Fragment.types = {}
Fragment.types[1] = function()
    line(0,7,-3,-8)
    line(-3,-8, 1, -4)
    line(1,-4, 0,7)
end

Fragment.types[2] = function()
    line(-2,2, 3,6)
    line(3,6, 3,-2)
    line(3,-2, -4,-4)
    line(-4,-4, -2,2)
end

Fragment.types[3] = function()
    line(-4,1, 3,3)
    line(3,3, 3,1)
    line(3,1, -1,-3)
    line(-1,-3, -5,-3)
    line(-5,-3, -4,1)
end

Fragment.types[4] = function()
    line(-5,2, -1,2)
    line(-1,2, 3,6)
    line(3,6, 6,-2)
    line(6,-2, -6,-2)
    line(-6,-2, -5,2)
end

function Fragment:init(fragmentNumber, ship)
    self.type = fragmentNumber
    self.cannotCollide = true
    self.pos = ship.pos
    local speed = 1 + math.random()
    local outwardVelocity = vec2(0,speed):rotate(math.random()*math.pi*2)
    self.vel = ship.vel + outwardVelocity
    self.angularVelocity = math.random()*10-5
    self.heading = 0
    self.timeOut = math.random(200,300)
    U:addObject(self)
end

function Fragment:move()
    self.vel = self:adjustedVelocity(self.vel)
    self.pos = U:clip_to_screen(self.pos + self.vel)
    self.heading = self.heading + self.angularVelocity
end

function Fragment:adjustedVelocity(vel)
    proposedV = vel + U:gravity(self)
    return U:limitSpeed(self.MaxSpeed, proposedV)
end

function Fragment:draw()
    self.timeOut = self.timeOut - 1
    if self.timeOut < 0 then self:die() end 
    pushMatrix()
    pushStyle()
    translate(self.pos:unpack())
    rotate(self.heading)
    self:drawFragment(self.type)
    popStyle()
    popMatrix()
end

function Fragment:drawFragment(type)
    strokeWidth(2)
    if self.types[type] then self.types[type]() else
        pushStyle()
        fill(255)
        text("?",0,0)
        popStyle()
    end
end

function Fragment:hitBy(anObject)
    self:die()
end

function Fragment:die()
    U:kill(self)
end

--# Sun
Sun = class()

function Sun:init()
    self.pos = vec2(WIDTH/2, HEIGHT/2)
    self.name = "Sun"
    self.damageDealt = 100
    U:addObject(self)
    U:addTouched(self)
end

function Sun:draw()
    pushMatrix()
    pushStyle()
    strokeWidth(2)
    translate(self.pos:unpack())
    stroke(216, 168, 48, 255)
    fill(216, 168, 48, 255)
    ellipse(0,0,10)
    popStyle()
    popMatrix()
end

function Sun:move()
end

function Sun:hitBy(anObject)
    
end

function Sun:touched(touch)
    if touch.state == BEGAN and self:nearMe(touch) then
        displayMode(STANDARD)
        setup()
    end
end

function Sun:nearMe(touch) 
    return self.pos:dist(vec2(touch.x,touch.y)) < 100
end

--# Universe
-- HAVE YOU PUSHED TO GITHUB TODAY?

Universe = class()

function Universe:init()
    self.Adds = {}
    self.Drawn = {}
    self.Removes = {}
    self.Touched = {}
    self.MissileKillDistance = 20
    self.GravitationalConstant = 200
    self.Tick = 0
end

function Universe:draw()
    U.Tick = U.Tick + 1
    self:updateUniverse()
    self:moveAll()
    self:interactAll()
    self:drawAll()
end

function Universe:touched(touch)
    for _,obj in ipairs(self.Touched) do
        obj:touched(touch)
    end
end

-- helper functions

function Universe:addInNewObjects()
    for _, obj in ipairs(self.Adds) do
        self.Drawn[obj] = obj
    end
    self.Adds = {}
end

function Universe:addObject(anObject)
    table.insert(self.Adds, anObject)
end

function Universe:addTouched(anObject)
    table.insert(self.Touched, anObject)
end

function Universe:clip_to_screen(vec)
    return vec2(vec.x%WIDTH, vec.y%HEIGHT)
end

function Universe:colliding(obj1, obj2)
    if obj1 == nil then return false end
    if obj2 == nil then return false end
    if obj1.cannotCollide then return false end
    if obj2.cannotCollide then return false end
    if obj1 == obj2 then return false end
    local d = obj1.pos:dist(obj2.pos)
    local result = d < U.MissileKillDistance
    return result
end

function Universe:drawAll()
    background(40, 40, 50)
    for _, obj in pairs(U.Drawn) do
        obj:draw()
    end
end

function Universe:gravity(anObject)
    local directionToSun = (U.sun.pos - anObject.pos):normalize()
    local distSquared = anObject.pos:distSqr(U.sun.pos)
    return U.GravitationalConstant*directionToSun/distSquared
end

function Universe:interactAll()
    for _, a in pairs(U.Drawn) do
        for _, b in pairs(U.Drawn) do
            if U:colliding(a,b) then
                a:hitBy(b)
            end
        end
    end
end

function Universe:kill(anObject)
    table.insert(self.Removes, anObject)
end

function Universe:limitSpeed(max, vel)
    local speed = vel:len()
    if speed > max then
        vel = vel*max/speed
    end
    return vel
end

function Universe:moveAll()
    for _, obj in pairs(U.Drawn) do
        obj:move()
    end
end

function Universe:removeDeadObjects()
    for _, obj in ipairs(self.Removes) do
        self.Drawn[obj] = nil
    end
    self.Removes = {}
end

function Universe:updateUniverse()
    self:removeDeadObjects()
    self:addInNewObjects()
end


--# Button
-- HAVE YOU PUSHED TO GITHUB TODAY?

Button = class()

function Button:init(x, y, w2, h2)
    self.name = string.format("Button %f %f", x, y)
    self.pos = vec2(x,y)
    self.width = w2
    self.height = h2
    self.pressed = false
    self.capturedID = nil
    self.cannotCollide = true
    U:addTouched(self)
    U:addObject(self)
end

function Button:draw()
    pushStyle()
    pushMatrix()
    rectMode(RADIUS)
    translate(self.pos:unpack())
    strokeWidth(1)
    stroke(255)
    fill(self:fillColor())
    rect(0,0, self.width, self.height)
    popMatrix()
    popStyle()
end

function Button:fillColor()
    if (self.pressed) then
        return color(0, 255, 0, 255)
    else
        return color(255,0,0,26)
    end
end

function Button:touched(touch) 
    self.capturedID = self:getCapture(touch)
    self.pressed = self:getPressed(touch)
end

function Button:getCapture(touch) 
    if touch.state == BEGAN and self:insideMe(touch) and not self.capturedID then
        return touch.id 
    elseif touch.state == ENDED and self.capturedID == touch.id then
        return nil
    else
        return self.capturedID
    end
end

function Button:getPressed(touch)
    if not self.capturedID then
        return false
    elseif self.capturedID ~= touch.id then
        return self.pressed
    else 
        return self:insideMe(touch)
    end
end

function Button:insideMe(touch)
    local x = touch.x
    local y = touch.y
    local myLeft   = self.pos.x - self.width
    local myRight  = self.pos.x + self.width
    local myBottom = self.pos.y - self.height
    local myTop    = self.pos.y + self.height
    local insideX = x >= myLeft and x <= myRight
    local insideY = y >= myBottom and y <= myTop
    return insideX and insideY
end

function Button:move()
    -- don't
end

--# Main
-- HAVE YOU PUSHED TO GITHUB TODAY?

-- S3 Spacewar

-- The Universe

U = Universe()

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

function automate()
    Missile(Ship1, vec2(0,10))
    Ship1.heading = Ship1.heading + 90
    Missile(Ship1, vec2(0,10))
end

function draw()
    U:draw()
end

function touched(touchid)
    U:touched(touchid)
end

function newGame()
    --displayMode(FULLSCREEN_NO_BUTTONS)
    U:init()
    U.sun = Sun()
    Ship1 = Ship(1)
    Ship(2)
end

--[[
function allFunctions(aClass) 
    names = {}
    for k,v in pairs(aClass) do
        print(k,v)
        if type(v) == "function" then table.insert(names,k) end
    end
    table.sort(names)
    ans = ""
    for i,n in pairs(names) do
        ans = ans.."\n"..n
    end
    print(ans)
end
]]--
--# Missile
-- HAVE YOU PUSHED TO GITHUB TODAY?

Missile = class()

Missile.count = 0

function Missile:init(ship, testVel, startPos)
    local myVel = testVel or vec2(0,1)
    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:move()
    self.distance = self.distance + self.vel:len()
    if self.distance > self.maxDistance then
        self:die()
        return
    end
    self.pos = U:clip_to_screen(self.pos + self.vel)
end

function Missile:draw()
    pushMatrix()
    pushStyle()
    strokeWidth(1)
    stroke(255,255,255,255)
    fill(255,255,255,255)
    translate(self.pos.x, self.pos.y)
    ellipse(0,0,5)
    popStyle()
    popMatrix()
end

function Missile:aWaysOutInFront(ship)
    local aWays = vec2(0, U.MissileKillDistance*1.5)
    return aWays:rotate(math.rad(ship.heading))
end

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

function Missile:die()
    U:kill(self)
end


--# Ship
-- HAVE YOU PUSHED TO GITHUB TODAY?

Ship = class()

    Ship.MaxMissiles = 50
    Ship.TicksBetweenMissiles = U.MissileKillDistance + 1
    Ship.MaxDamage = 5

function Ship:init(shipNumber)
    self.name = string.format("Ship %d", shipNumber)
    self.missileLoad = Ship.MaxMissiles
    self.timeLastFired = -Ship.TicksBetweenMissiles
    self.shipNumber = shipNumber
    self.hitpoints = Ship.MaxDamage
    self.damageDealt = 5
    
    self.pos = self:initialPosition()
    self.heading = self:initialHeading()
    self.vel = vec2(0,0)
    self.controls = { 
        left   = self:controlButton(85, HEIGHT-50,  85, 50),
        right  = self:controlButton(50, HEIGHT-185, 50, 85),
        thrust = self:controlButton(50, 185,        50, 85),
        fire   = self:controlButton(85, 50,         85, 50)
    }
    U:addObject(self)
end

function Ship:controlButton(x,y, w2, h2)
    if self.shipNumber == 2 then
        return Button(x,y, w2, h2)
    else
        return Button(WIDTH-x,HEIGHT-y, w2, h2)
    end
end

function Ship:initialPosition()
    if self.shipNumber == 1 then
        return vec2(WIDTH/2, HEIGHT/2 + 200)
    else
        return vec2(WIDTH/2, HEIGHT/2 - 200)
    end
end

function Ship:initialHeading()
    if self.shipNumber == 1 then
        return 90
    else
        return 270
    end
end

function Ship:move()
    local turn
    local thrust
    
    if     self.controls.left.pressed  and not self.controls.right.pressed then turn =  Turn
    elseif self.controls.right.pressed and not self.controls.left.pressed  then turn = -Turn
    else turn = 0 end
    self.heading = self.heading + turn
    
    if self.controls.thrust.pressed then thrust = vec2(0, ThrustAmount) else thrust = vec2(0,0) end
    self.vel = self:adjustedVelocity(self.vel, self.heading, thrust)
    self.pos = U:clip_to_screen(self.pos + self.vel)
    
    if self.controls.fire.pressed then self:fireMissile() end
end

function Ship:fireMissile()
    if self.missileLoad > 0 and U.Tick - self.timeLastFired >= Ship.TicksBetweenMissiles then
        self.timeLastFired = U.Tick
        self.missileLoad = self.missileLoad - 1 
        Missile(self)
    end
end

function Ship:draw()
    pushMatrix()
    pushStyle()
    strokeWidth(2)
    translate(self.pos:unpack())
    rotate(self.heading)
    line(0, 15, 5, -15)
    line(5, -15, -5, -15)
    line(-5, -15, 0, 15)
    popStyle()
    popMatrix()
end

function Ship:adjustedVelocity(vel, heading, accel)
    local proposed = vel + accel:rotate(math.rad(heading))
    proposed = proposed + U:gravity(self)
    return U:limitSpeed(VMax, proposed)
end

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

function Ship:die()
    self:explode()
    U:kill(self)
end

function Ship:explode()
    for i = 1, 5 do
        Fragment(i, self)
    end
end

--# TestButtonCU
-- HAVE YOU PUSHED TO GITHUB TODAY?

function testSpacewarButton()
    CodeaUnit.detailed = false
    local dSave
    local tSave
    
    local insideEndTouch    = { id=1, state=ENDED, x=100, y=100 }
    local insideBeganTouch  = { id=3, state=BEGAN, x=100, y=100 }
    local outsideBeganTouch = { id=4, state=BEGAN, x=151, y=100 }
    local closeThree = { id=3, state=ENDED, x=200, y=200 }
    local movingFinger = { id=5, state=BEGAN, x = 100, y=100}
    local button
    
    _:describe("Spacewar Button Test", function()
        
        _:before(function()
            dSave = U.Drawn
            tSave = U.Touched
            U.Drawn = {}
            U.Touched = {}
            button = Button(100, 100)
        end)
        
        _:after(function()
            U.Drawn = dSave
            U.Touched = tSave
        end)
        
        _:test("No global button", function()
            _:expect(_G["button"]).is(nil)
        end)
        
        _:test("Inside Circle", function()
            _:expect(button:insideMe(insideEndTouch)).is(true)
            _:expect(button:insideMe(outsideBeganTouch)).is(false)
        end)
        
        _:test("Initial Button Capture", function()
            button:touched(insideBeganTouch)
            _:expect(button.capturedID).is(3)
            button:touched(outsideBeganTouch)
            _:expect(button.capturedID).is(3)
            _:expect(button.pressed).is(true)
        end)
        
        _:test("Sequence Test", function()
            button:touched(closeThree)
            _:expect(button.capturedID).is(nil)
            button:touched(movingFinger)
            _:expect(button.capturedID).is(5)
            _:expect(button.pressed).is(true)
            movingFinger.state=MOVING
            button:touched(movingFinger)
            _:expect(button.pressed).is(true)
            movingFinger.x = 200
            button:touched(movingFinger)
            _:expect(button.pressed).is(false) -- no longer pressed
            _:expect(button.capturedID).is(5) -- but retains capture
        end)
        
        _:test("Two Fingers", function()
            button:touched(insideBeganTouch)
            _:expect(button.capturedID).is(3)
            _:expect(button.pressed).is(true)
            button:touched(outsideBeganTouch)
            _:expect(button.capturedID).is(3)
            _:expect(button.pressed).is(true)
        end)
    end)
end

--# TestControlPlacement
-- HAVE YOU PUSHED TO GITHUB TODAY?

function testControlPlacement()
    local dSave
    local tSave
    _:describe("Control Placement", function()
        
        _:before(function()
            dSave = U.Drawn
            tSave = U.Touched
            U.Drawn = {}
            U.Touched = {}
        end)
        
        _:after(function()
            U.Drawn = dSave
            U.Touched = tSave
        end)
        
        _:test("hookup", function()
            _:expect("hookup").is("hookup")
        end)
        
        _:test("right ship buttons", function()
            local rs = Ship(1)
          --  _:expect(rs.controls.left.pos.y).is(200)
            _:expect(rs.controls.left.pos.x).is(WIDTH - 85)
        end)
        
        _:test("left ship buttons", function()
            local ls = Ship(2)
         --   _:expect(ls.controls.left.pos.y).is(HEIGHT-200)
            _:expect(ls.controls.left.pos.x).is(85)
        end)
        
    end)
end

--# TestKill
-- HAVE YOU PUSHED TO GITHUB TODAY?

function testKill()
    
    _:describe("Missiles Kill", function()
            
        _:before(function()
            dSave = U.Drawn
            tSave = U.Touched
            U.Drawn = {}
            U.Touched = {}
        end)
        
        _:after(function()
            U.Drawn = dSave
            U.Touched = tSave
        end)
        
        _:test("All exist", function()
            Ship(1)
            Ship(2)
            U:updateUniverse()
            local found = {}
            for _, obj in pairs(U.Drawn) do
                found[obj] = obj.name
            end
            _:expect(found).has("Ship 1")
            _:expect(found).has("Ship 2")
        end)
        
        _:test("Ship no longer kills self by firing missile", function()
            local ship1 = Ship(1)
            Ship(2)
            Missile(ship1)
            U:updateUniverse()
            U:interactAll()
            local found = {}
            for _, obj in pairs(U.Drawn) do
                found[obj] = obj.name
            end
            _:expect(found).has("Ship 1")
            _:expect(found).has("Ship 2")
        end)
        
    end)
end

--# TestPairs
-- HAVE YOU PUSHED TO GITHUB TODAY?

function testPairs()
    local items = {a = "a", b = "b", c = "c"}
    _:describe("Test Skipping Pairs", function()
        _:test("Find nine without considering formers", function()
            local count = 0
            for _, obj in pairs(items) do
                for _, other in pairs(items) do
                    count = count + 1
                end
            end
            _:expect(count).is(9)
        end)
        
        _:test("Find six (three choose two) with considering formers", function()
            local count = 0
            local considered = {}
            for _, obj in pairs(items) do
                for _, other in pairs(items) do
                    if considered[other] and considered[other][obj] then
                        -- skipped
                    else
                        if not considered[obj] then considered[obj] = {} end
                        considered[obj][other] = true
                        count = count + 1
                    end
                end
            end
            _:expect(count).is(6)
        end)
    end)
end
  1. In movies, apparently this happens. In software, it never does. I am saying it here ironically but you get to mock me if we forget. 

  2. Ah’m givin’ ye all she’s got, Cap’n.