On a collision course …

When last we met, it was Friday, and we had devised some simple Missiles that ships could fire. The missiles know to die after a while, but they don’t do anything interesting. And they don’t do any damage either. Let’s see if we can make them kill a ship or something. We’ve not looked at all the code for a while, so let’s begin by doing that and then see what that makes us think.


--# Button
Button = class()

function Button:init(x, y)
    self.pos = vec2(x,y)
    self.radius = 50
    self.pressed = false
    self.capturedID = nil
    addTouched(self)
    addDrawnAtBottom(self)
end

function Button:draw()
    pushStyle()
    pushMatrix()
    ellipseMode(RADIUS)
    translate(self.pos:unpack())
    fill(self:fillColor())
    ellipse(0,0, self.radius)
    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:insideCircle(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:insideCircle(touch)
    end
end

function Button:insideCircle(touch)
    return self.pos:dist(vec2(touch.x, touch.y)) <= self.radius
end

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

--# Main
-- S3 Spacewar

-- The Universe

U = {}
U.Tick = 0
U.Drawn = {}
U.Touched = {}

function setup()
    Ship(1)
    Ship(2)
end

function touched(touchid)
    for _,obj in ipairs(U.Touched) do
        obj:touched(touchid)
    end
end

function draw()
    U.Tick = U.Tick + 1
    background(40, 40, 50)
    
    for _,obj in ipairs(U.Drawn) do
        obj:move()
    end
    
    for _,obj in ipairs(U.Drawn) do
        obj:draw()
    end
end

function addTouched(anObject)
    table.insert(U.Touched, anObject)
end

function addDrawnAtTop(anObject)
    table.insert(U.Drawn, anObject)
end

function addDrawnAtBottom(anObject)
    table.insert(U.Drawn, 1, anObject)
end

--# Missile
Missile = class()

function Missile:init(ship)
    self.pos = ship.pos
    local velocity = vec2(0,1):rotate(math.rad(ship.heading))
    self.vel = ship.vel + velocity
    self.maxDistance = WIDTH*1.4
    self.distance = 0
    self.alive = true
    addDrawnAtTop(self)
end

function Missile:move()
    self.distance = self.distance + self.vel:len()
    if self.distance > self.maxDistance then
        self.alive = false
        return
    end
    self.pos = clip_to_screen(self.pos + self.vel)
end

function Missile:draw()
    pushMatrix()
    pushStyle()
    strokeWidth(1)
    stroke(255,0,0,255)
    fill(255,0,0,255)
    translate(self.pos.x, self.pos.y)
    if ( self.alive ) then
        ellipse(0,0,3)
    else
        ellipse(0,0,10)
    end
    popStyle()
    popMatrix()
end


--# Ship
Ship = class()

function Ship:init(shipNumber)
    local missilesPerSecond = 3
    self.MissileMod = math.ceil(50/missilesPerSecond)
    self.missileLoad = 50
    self.timeLastFired = -self.MissileMod
    self.shipNumber = shipNumber
    self.ThrustAmount = vec2(0, 0.02)
    self.TurnAmount = 1
    self.MaxSpeed = 3
    
    self.pos = self:initialPosition()
    self.heading = self:initialHeading()
    self.vel = vec2(0,0)
    self.controls = { 
        left   = self:controlButton(HEIGHT/5),
        right  = self:controlButton(2*HEIGHT/5),
        thrust = self:controlButton(3*HEIGHT/5),
        fire   = self:controlButton(4*HEIGHT/5)
    }
    addDrawnAtTop(self)
end

function Ship:controlButton(offset)
    if self.shipNumber == 1 then
        return Button(WIDTH-85, offset)
    else
        return Button(85, HEIGHT-offset)
    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 =  self.TurnAmount
    elseif self.controls.right.pressed and not self.controls.left.pressed  then turn = -self.TurnAmount
    else turn = 0 end
    self.heading = self.heading + turn
    
    if self.controls.thrust.pressed then thrust = self.ThrustAmount else thrust = vec2(0,0) end
    self.vel = self:adjustedVelocity(self.vel, self.heading, thrust)
    self.pos = 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 > self.MissileMod then
        self.timeLastFired = U.Tick
        self.missileLoad = self.missileLoad - 1 
        Missile(self)
    end
end

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

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

function Ship:adjustedHeading(heading, turn)
    return heading + turn
end

function Ship:adjustedVelocity(vel, heading, accel)
    local proposed = vel + accel:rotate(math.rad(heading))
    local speed = proposed:len()
    if speed > self.MaxSpeed then
        proposed = proposed*self.MaxSpeed/speed
    end
    return proposed
end

--# TestButtonCU
function testSpacewarButton()
    CodeaUnit.detailed = false
    
    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()
            button = Button(100, 100)
        end)
        
        _:test("No global button", function()
            _:expect(_G["button"]).is(nil)
        end)
        
        _:test("Inside Circle", function()
            _:expect(button:insideCircle(insideEndTouch)).is(true)
            _:expect(button:insideCircle(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
function testControlPlacement()
    _:describe("Control Placement", function()
        
        _: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

Looking at Button, I’m reminded that the capture / pressed logic looks a bit weird, but it’s working now so I don’t want to go after it.

Main seems mostly OK. Probably we should think about moving the addTouched and addDrawn ideas to U, the universe, but that’s not calling out very loudly.

I could imagine rearranging the code a bit in Missile, but that’s not calling out either. I notice that missiles die after a maximum distance, not a maximum time. Should a faster missile be able to go further in its lifetime? One might think so. Let’s make a note of that but leave it for now.

Ship is getting a bit busy. Possibly a bit of function extraction will help make it more expressive. I’m not noticing any new classes trying to be born. The clip_to_screen function has a weird name and probably belongs in some other place, since it’s really global and is used in Missile as well as in Ship. I’ll move that up to Main, but that’s not a final resting place either. Maybe it’s part of the universe? But remember, right now, Universe is just a table. We’ll see what the code tells us as time goes on. For now, I’m moving the clip to Main. Moved, ran the program, still works.

OK, none of this is interesting enough to do anything about. Let’s work on collisions.

Right now, it seems to me, everything Drawn can collide. So let’s do a collision thing in the draw function of Main, which looks like this now:

function draw()
    U.Tick = U.Tick + 1
    background(40, 40, 50)
    
    for _,obj in ipairs(U.Drawn) do
        obj:move()
    end
    
    for _,obj in ipairs(U.Drawn) do
        obj:draw()
    end
end

For a first cut, we’ll just loop over all pairs of objects and see if they collide. Since our objects are in array form, we could probably code up something that wasn’t N-squared, but let’s just do the simplest possible thing:

    for _,obj in ipairs(U.Drawn) do
        for _,other in ipairs(U.Drawn) do
            if obj:colliding(other) then
                
            end
        end
    end

Yeah … then what? Presumably the objects both die, and I suppose we’d like to add an explosion. But we’re in the middle of looping over our main table, so we can’t just go hammering about in it. I think I’ll make a table of dead things to remove, and a table of new things to add:

    local removes = {}
    
    for iObj,obj in ipairs(U.Drawn) do
        for iOther,other in ipairs(U.Drawn) do
            if obj:colliding(other) then
                table.insert(removes,iObj)
                table.insert(removes.iOther)
            end
        end
    end
    
    for _,remove in ipairs(removes) do
        table.remove(U.Drawn, remove)
    end

If this works, I expect that hitting a ship with a missile will make the ship and the missile disappear. Let me try that. Well, first we’d better implement colliding on ships and missiles. (Hmm, duplication. That’s bad but for now I’ll fitness it with a local change.)

    for iObj,obj in ipairs(U.Drawn) do
        for iOther,other in ipairs(U.Drawn) do
            if colliding(obj, other) then
                table.insert(removes,iObj)
                table.insert(removes, iOther)
            end
        end
    end
    
    for _,remove in ipairs(removes) do
        table.remove(U.Drawn, remove)
    end
    -- and
    
function colliding(obj1, obj2)
    if obj1 == obj2 then return false end
    return obj1.pos:dist(obj2.pos) < 20
end

This has two surprising effects when I fire a missile. First, the firing ship disappears. Since the missile starts at ship center, well, it’s in range and kills the firing ship. More interesting is the fact that the other ship vanishes as well. Makes me think it also got killed. But why?

A little judicious printing tells me. Our loop scans all the objects twice, as remarked. When a collision occurs, we record the record numbers of the colliding objects. Then we record them again. This won’t work at all. First, suppose they are #9 and #11. Removing 11 and then 9 will work. Removing 9 and then 11 will not work. Worse yet, removing 9 and 11 again will delete two innocent objects from the game.

Our array scheme won’t work, and our double loop scheme is at least iffy. What can we do?

This, by the way, is an example of when someone pairing with me would be useful, or someone to talk to. I don’t think the young lady at the next table would like to discuss this, however, so I’m on my own. I’ll try to think of a number of ideas, so I can pick the simplest one that might possibly work.

  • Fix the double loop, then sort the removes high to low and remove them.
  • Remove duplicates from removes, sort high to low, remove
  • Use a keyed table, not an array. Remove things by setting them nil. Double setting should be harmless. Might even be able to avoid the remove loop.

The first two I could just recode though I’m not sure how to do either one of them. The latter seems better but it is a more major restructuring of the Drawn table. Strictly speaking, I should back out my collision logic and get back to a “green bar” but I think I’ll go forward because all the code should be local to Main.

However, the phrase “green bar” reminds me that this is being done without automated tests. Is there something here where a test would help? Perhaps there is but I am inclined to push on. (“The big fool said to push on.”) A test could be good though. What would it look like? Perhaps like this:

  1. Build a universe with two ships.
  2. Check that they’re there.
  3. Add a missile in collision with one of the ships.
  4. Check that the missile is gone, the right ship is gone, and the other ship is present.

This might work. Now a moral dilemma almost as bad as whether to eat a donut just because there is a donut present (my usual donut strategy). Do the “right” thing and write the test, or do the “right” thing and make the code work? For once, I’ll try to write the test, but believe me, if it gets hard I’m going back to coding. Just like you would, don’t lie to me.

Wow. I wrote this simple first test to see if I had two ships:

function testKill()
    
    _:describe("Missiles Kill", function()
        
        _:test("All exist", function()
            local found = {}
            for _, obj in ipairs(U.Drawn) do
                table.insert(found, obj.name)
                print(obj.name)
            end
            _:expect(found).has("Ship 1")
            _:expect(found).has("Ship 2")
        end)
        
    end)
end

I expected that to pass. And it nearly does. But I can hardly tell: lots of other tests are throwing exceptions as the system tries to delete Buttons and other odds and ends. There seem to be a lot more objects in here than should be. I’m seeing multiple copies of the main control buttons, and they are of course colliding. I can probably fix that (and a double dispatch comes to mind here) but I’m not even sure what’s really going on. Well, let’s see. The TestControlPlacement test creates two ships and checks their buttons. This ensures there are at least twice as many ships and buttons as we need. Maybe we should make that test clear the Drawn table.

Now believe me, I really want to stop this right now. Testing a game like this is a bad idea, any fool can see this now. Except that we just wrote some logic a few minutes ago that doesn’t work, and that calls for tests. Let’s see if we can isolate some of the tests by clearing Drawn and maybe Touched:

function testControlPlacement()
    _:describe("Control Placement", function()
        
        _:before(function()
            U.Drawn = {}
            U.Touched = {}
        end)
        
        ...
end

This makes everything moderately happy, but the All exist test doesn’t quite pass:

“1: All exist – Actual: table: 0x1469483f0, Expected: Ship 1”

Perhaps I don’t understand how the .has function works. Let’s see what I did wrong. Ah. has expects that the table is keyed. It uses pairs, not `ipairs’, which is almost convenient since converting to that kind of table is what I have in mind. Let’s just do that.

Some time later …

Wow. Never, ever, say “just”. Here are the changes to Main that I set out to do:

function draw()
    U.Tick = U.Tick + 1
    background(40, 40, 50)
    
    for _,obj in pairs(U.Drawn) do
        obj:move()
    end
    
    local removes = {}
    
    for iObj,obj in pairs(U.Drawn) do
        for iOther,other in pairs(U.Drawn) do
            if colliding(obj, other) then
                --print("removing", iObj, obj.name, iOther, other.name)
            end
        end
    end
    
    for _,obj in pairs(U.Drawn) do
        obj:draw()
    end
end

function addDrawnAtTop(anObject)
    U.Drawn[anObject] = anObject
end

function addDrawnAtBottom(anObject)
    U.Drawn[anObject] = anObject
end

OK, basically we convert U:Drawn to a keyed table just by storing K-V pairs. I just use the object as its own key, which works just fine. This code ran the game perfectly. We note in passing that we’ve lost the top vs bottom distinction, since keyed tables iterate in random order. I’m planning on that not being a problem in the real game, but we’ll see.

To make the tests run decently, I had to cause them all to save the game tables, clear them, use them, and restore them, with code like this:

        _: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)

That’s not awful, though it is duplicated, which tells me something needs to be done. But I’m less strict about duplication in my tests, so I don’t feel too badly about it. I now have my initial kill test working, and all the other tests are happy also.

function testKill()
    
    _:describe("Missiles Kill", function()
        
        _:test("All exist", function()
            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

If I recall the plan, I was going to add a missile that killed one of the ships, and then see whether the other ship was still there. There has been a fair amount of yak-shaving since then but I’m nearly ready. However …

While all this testing is going on, the game is running. Ships are flying and so on. A quick touch of the screen after running the tests tells me that the Touched table is gone. Did you notice in the save / restore that it said U.touched, not U.Touched? Neither did I. OK, now the game stays alive. But so what?

I think that for this test, I’d like the game to stop. Then I’d like to set things up and run the collision logic just once. After that, the one ship should be gone, and the other one should still be there. Yabbut the system doesn’t work that way. There’s no way to stop or pause the game, and no way to run the collision logic just once.

This is a form of good news. When the code is hard to test, odds are the code is not well factored. So let’s do two things: Add a U.running flag to pause the game logic, and then refactor enough so that we can do collision testing on its own. As we go, we’ll have to make our test run, which is the point after all.

First:

function draw()
    if not U.Running then return end
    
    U.Tick = U.Tick + 1
    background(40, 40, 50)
    
    for _,obj in pairs(U.Drawn) do
        obj:move()
    end
    
    collisions()
    
    for _,obj in pairs(U.Drawn) do
        obj:draw()
    end
end

function collisions()
    for iObj,obj in pairs(U.Drawn) do
        for iOther,other in pairs(U.Drawn) do
            if colliding(obj, other) then
                U.Drawn[obj] = nil
                U.Drawn[other] = nil
            end
        end
    end
end

function colliding(obj1, obj2)
    if obj1 == nil then return false end
    if obj2 == nil then return false end
    if obj1 == obj2 then return false end
    return obj1.pos:dist(obj2.pos) < 20
end

If we’re not running, the draw() function just exits. If we are running, it calls a new function, collisions. Checking whether two objects are colliding first checks to be sure that neither of them is nil. If they do collide, the collisions function sets them nil. This will not be spectacular but it should be sufficient to make the colliding ship and missile disappear.

I was concerned, of course, whether I had broken the game, so I ran it. When I did so, and hit the fire button, the firing ship disappeared, as did the missile. It looks like this:

MOVIE HERE mutual-destruction

So that’s good news: I know my new logic works. But let’s finish the testing part and then sum up.

        _:test("Ship dies", function()
            local ship1 = Ship(1)
            Ship(2)
            Missile(ship1)
            collisions()
            local found = {}
            for _, obj in pairs(U.Drawn) do
                found[obj] = obj.name
            end
            _:expect(found).hasnt("Ship 1")
            _:expect(found).has("Ship 2")
        end)

This doesn’t quite run, because CodeaUnit doesn’t understand hasnt. So I upgraded my testing framework:

    local hasnt = function(expected)
        self.expected = expected
        local missing = true
        for i,v in pairs(conditional) do
            if v == expected then
                missing = false
            end
        end
        notify(missing)
    end

Of course, I also added hasnt to the returned table in expect:

    return {
        is = is,
        isnt = isnt,
        has = has,
        hasnt = hasnt,
        throws = throws
    }

Now my tests run and the game runs. Of course the test we just wrote is wrong, because Ship 1 shouldn’t die for firing a missile. The missile has to rez outside the Ship’s own kill radius. (We could do something more clever, like make the missile inactive for a while or the like. We’ll decide next time, but probably won’t do anything that clever.) For now, let’s sum up.

What just happened?

Well, we have a rudimentary kill capability built. We can be pretty sure that if a missile hits a ship, the ship will die. We didn’t test to see whether a missile hitting a missile will kill both, but my guess is that it will. The kill isn’t pretty, but it seems to be working as I’d expect at this moment. I’ll add some notes below. But the big question, to me, is whether trying to write that test was a bad idea.

After all, I had to invent U.Running (and it isn’t really used: we never turned it off. Instead, we did use the extracted collisions function, and we killed our local ship and its missile before restoring the game’s versions. So we should look at that and see whether to pause the game for our tests. We can be sure that draw() won’t be called during the tests, so maybe we built U.Running prematurely.

We needed to change how the Drawn objects are represented, moving away from the indexed table to a keyed table. So that’s OK. It was useful and a good idea for the tests to clean up the game space before starting and put it back when done. So that’s a righteous improvement. And we had to upgrade our testing framework to add hasnt. That seems like a lot of yak shaving.

However, it is 12:01 as I write this, and I got started a little after 9:00. So in three hours, I have what I set out to do, get the rudiments of colliding objects working, with an improvement to the overall structure (the keyed table rather than indexed), some real certainty that things are happening, a stronger set of tests, and a stronger testing framework.

Am I rationalizing my decision? Not so much, but I’m sure looking back at it and trying to draw lessons. It would have been really easy not to try to write that test. And had I not done so, the keyed table solution was still on my mind, and as soon as I put it in, the firing ship disappeared, telling me that there was an issue with spawning the missile and killing its owner, and the other ship didn’t disappear, which was as it should be until I can stop killing myself with my own missiles. I saw that happen on the screen very early on. All this work with figuring out how to test a kill, and how to keep the tests from contaminating the game space, and how to make the testing framework cope with hasnt … those were all unnecessary. I could have made it work, without all that.

I think I’m glad I decided not to eat the donut, and to write the test. I think the system is more robust now, and more testable, and somewhat better factored (since I had to pull out the collisions capability to test it). Did I waste that time? Or did I learn enough, and improve enough, to make it worth having done? I’m saying yes. If I had to make a suggestion to you, I’d suggest that once in a while when you get that nagging feeling about writing a test but you know it’ll be a pain, maybe you might go ahead and write the test, not giving up until it’s done right, and then decide what you think.

And catch me somewhere and tell me about it. Me, I’m happy with where we are for now.


--# Button
Button = class()

function Button:init(x, y)
    self.name = string.format("Button %f %f", x, y)
    self.pos = vec2(x,y)
    self.radius = 50
    self.pressed = false
    self.capturedID = nil
    addTouched(self)
    addDrawnAtBottom(self)
end

function Button:draw()
    pushStyle()
    pushMatrix()
    ellipseMode(RADIUS)
    translate(self.pos:unpack())
    fill(self:fillColor())
    ellipse(0,0, self.radius)
    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:insideCircle(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:insideCircle(touch)
    end
end

function Button:insideCircle(touch)
    return self.pos:dist(vec2(touch.x, touch.y)) <= self.radius
end

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

--# Main
-- S3 Spacewar

-- The Universe

U = {}
U.Tick = 0
U.Drawn = {}
U.Touched = {}
U.Running = true

function setup()
    Ship(1)
    Ship(2)
    U.Running = true
end

function touched(touchid)
    for _,obj in ipairs(U.Touched) do
        obj:touched(touchid)
    end
end

function draw()
    if not U.Running then return end
    
    U.Tick = U.Tick + 1
    background(40, 40, 50)
    
    for _,obj in pairs(U.Drawn) do
        obj:move()
    end
    
    collisions()
    
    for _,obj in pairs(U.Drawn) do
        obj:draw()
    end
end

function collisions()
    for iObj,obj in pairs(U.Drawn) do
        for iOther,other in pairs(U.Drawn) do
            if colliding(obj, other) then
                U.Drawn[obj] = nil
                U.Drawn[other] = nil
            end
        end
    end
end

function addTouched(anObject)
    table.insert(U.Touched, anObject)
end

function addDrawnAtTop(anObject)
    U.Drawn[anObject] = anObject
end

function addDrawnAtBottom(anObject)
    U.Drawn[anObject] = anObject
end

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

function colliding(obj1, obj2)
    if obj1 == nil then return false end
    if obj2 == nil then return false end
    if obj1 == obj2 then return false end
    return obj1.pos:dist(obj2.pos) < 20
end

--# Missile
Missile = class()

function Missile:init(ship)
    self.name = "missile"
    self.pos = ship.pos
    local velocity = vec2(0,1):rotate(math.rad(ship.heading))
    self.vel = ship.vel + velocity
    self.maxDistance = WIDTH*1.4
    self.distance = 0
    self.alive = true
    addDrawnAtTop(self)
end

function Missile:move()
    self.distance = self.distance + self.vel:len()
    if self.distance > self.maxDistance then
        self.alive = false
        return
    end
    self.pos = clip_to_screen(self.pos + self.vel)
end

function Missile:draw()
    pushMatrix()
    pushStyle()
    strokeWidth(1)
    stroke(255,0,0,255)
    fill(255,0,0,255)
    translate(self.pos.x, self.pos.y)
    if ( self.alive ) then
        ellipse(0,0,3)
    else
        ellipse(0,0,10)
    end
    popStyle()
    popMatrix()
end


--# Ship
Ship = class()

function Ship:init(shipNumber)
    self.name = string.format("Ship %d", shipNumber)
    local missilesPerSecond = 3
    self.MissileMod = math.ceil(50/missilesPerSecond)
    self.missileLoad = 50
    self.timeLastFired = -self.MissileMod
    self.shipNumber = shipNumber
    self.ThrustAmount = vec2(0, 0.02)
    self.TurnAmount = 1
    self.MaxSpeed = 3
    
    self.pos = self:initialPosition()
    self.heading = self:initialHeading()
    self.vel = vec2(0,0)
    self.controls = { 
        left   = self:controlButton(HEIGHT/5),
        right  = self:controlButton(2*HEIGHT/5),
        thrust = self:controlButton(3*HEIGHT/5),
        fire   = self:controlButton(4*HEIGHT/5)
    }
    addDrawnAtTop(self)
end

function Ship:controlButton(offset)
    if self.shipNumber == 1 then
        return Button(WIDTH-85, offset)
    else
        return Button(85, HEIGHT-offset)
    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 =  self.TurnAmount
    elseif self.controls.right.pressed and not self.controls.left.pressed  then turn = -self.TurnAmount
    else turn = 0 end
    self.heading = self.heading + turn
    
    if self.controls.thrust.pressed then thrust = self.ThrustAmount else thrust = vec2(0,0) end
    self.vel = self:adjustedVelocity(self.vel, self.heading, thrust)
    self.pos = 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 > self.MissileMod then
        self.timeLastFired = U.Tick
        self.missileLoad = self.missileLoad - 1 
        Missile(self)
    end
end

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

function Ship:adjustedHeading(heading, turn)
    return heading + turn
end

function Ship:adjustedVelocity(vel, heading, accel)
    local proposed = vel + accel:rotate(math.rad(heading))
    local speed = proposed:len()
    if speed > self.MaxSpeed then
        proposed = proposed*self.MaxSpeed/speed
    end
    return proposed
end

--# TestButtonCU
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:insideCircle(insideEndTouch)).is(true)
            _:expect(button:insideCircle(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
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
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)
            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 dies", function()
            local ship1 = Ship(1)
            Ship(2)
            Missile(ship1)
            collisions()
            local found = {}
            for _, obj in pairs(U.Drawn) do
                found[obj] = obj.name
            end
            _:expect(found).hasnt("Ship 1")
            _:expect(found).has("Ship 2")
        end)
        
    end)
end

Notes

  • Missiles might time out, not distance out.
  • Missile should not kill the ship immediately upon firing.
  • Should ships be immune to their own missiles? I think not.
  • Make things explode, at least Ships.