Let’s create … THE UNIVERSE!

Today, as a result of a dream or something, I’ve decided that it’s time for the global object U, which contains the Drawn and Touched collections, and a few functions, to become a real class-based object. I plan to call the class Universe, and its instance will still be named U. My reasons for doing this are mostly about consistency: since other entities like Button, Ship, and Missile are objects, everything should be that way. In addition, I want to hide a bit more information, like the existence of the Drawn/Touched distinction, and I have in mind making it easier to run the tests independently of the game. (You may recall that the tests have a tendency to throw ships and buttons and things onto the screen, and there’s some saving and restoring going on in the tests that we can probably do better inside a Universe object.

Here’s what we have in U right now:

-- Main
-- The Universe

U = {}
U.Tick = 0
U.Adds ={}
U.Drawn = {}
U.Removes = {}
U.Touched = {}
U.Running = true
U.MissileKillDistance = 20
U.kill = function(objToRemove)
    table.insert(U.Removes, objToRemove)
end
U.add = function(objToAdd)
    table.insert(U.Adds, objToAdd)
end

I think it was the dot-functions, kill and add that really pushed me toward an object. Most functions in our game are called with colon, because they are methods on classes. That these two are different bugs me, and it is surely defect-prone, which bugs the program.1

I expect this to be a pretty simple refactoring, just add the class much like the setup code above, and then hook it up. Here goes:

More than an hour later …

The refactoring was a bit larger than I thought: I had to change more U.thisAndThat to U:thisAndThat, and the like. But it seemed to go OK until I ran the program. When I ran the program and fired a missile across the screen at myself, the Explosion appeared, but the Ship did not disappear, and the Missile did not disappear. Finally the Missile timed out, but it still didn’t disappear, it just sat there.

Then I did the bad thing: I decided to debug the problem. And, in fact, I did debug the problem, just now: somehow I was adding Drawn items in the old ipairs style, not the new pairs style. I’m not sure if that old code was there or if I typed it in wrong. Either way, a one line fix and it seems to be working.

But when we undertake a refactoring and have to hammer it to work, it is a sign from nature that we’ve taken too big a bite. The more nearly right thing to do when a refactoring doesn’t work is to back it out and do it again. And, very likely, we should do it in smaller steps. So that’s what I’m going to do. I’ve rolled back to my starting point, and I’m going to convert to a Universe class in smaller steps.

That’s going to be a bit tricky. I’ll build the class and move functionality over to it bit by bit. I think the right thing will be to create the Universe instance, make it known to the current U table, forward messages to it until it’s doing all the work, then remove the U table. The trickiest bit will be that U has some values that are directly accessed. Probably the best thing will be to give those accessors in Universe and use those. If we do that one at a time, we’ll write U:tick() where we used to have U.Tick and the U table will forward its tick() message to the Universe instance. We’ll have to fudge with the dot vs colon thing and that may be bad enough for this plan to be no good. We’ll try to choose things to refactor so as to find trouble early, in case the plan needs to be updated.

Here goes … again:

Universe = class()

function Universe:init(x)
end

function Universe:draw()
    -- Codea does not automatically call this method
end

function Universe:touched(touch)
    -- Codea does not automatically call this method
end

-- Main

-- The Universe

U = {}
U.universe = Universe()
U.Tick = 0
U.Adds ={}
U.Drawn = {}
U.Removes = {}
U.Touched = {}
U.Running = true
U.MissileKillDistance = 20
U.kill = function(objToRemove)
    table.insert(U.Removes, objToRemove)
end
U.add = function(objToAdd)
    table.insert(U.Adds, objToAdd)
end

All I’ve done here is create an empty Universe class, and instantiate one into U.universe. This, of course, runs fine. Well, almost. Because the Codea tabs are compiled in order from left to right, I have to drag the Universe class to the left, ahead of Main. Once I do that, it’s all good.

I’m really tempted to move drawing to Universe. Right now, it’s in Main:

function draw()
    U.Tick = U.Tick + 1
    remove()
    add()
    moveAll()
    interactAll()
    drawAll()
end

With all those little functions also inside Main, of course. However, they are global, so we should be able to move just this one function to Universe, and call it from Main. It will call back and everything should continue to work. Let’s try:

Universe = class()

function Universe:init(x)
end

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

function Universe:touched(touch)
    -- Codea does not automatically call this method
end

-- Main

function draw()
    U.universe:draw()
end

Works as intended. Of course it’s calling right back so no surprise. However, each change so far has taken only seconds and the system still works. That’s more like “Agile” as I understand it.

Note, however, that I’m testing by running the game. That tells me that my tests are not robust enough to give me confidence if I just run them. We’ll put that on the list for later but if we get in trouble again we’d better think about better tests. The tests do all run, however. One of them is still putting a spurious Button on the screen, because it doesn’t have that save/restore in it, but I plan to leave that until the Universe is created.

What next? For the drawing functions to run, we need the U.Drawn table to be supported by Universe. I can think of a few ways to get that happening:

  • maintain it for now in U, and jam a copy into U.universe
  • push it now to U.universe and refactor the U functions to call over there
  • push the touch logic over first, because it will be easier.

I’m nervous because of the previous screwup, so moving touch seems like I might learn something. My “hold my beer” personality component wants to go ahead with the harder thing but I think I’ll stay conservative for a bit longer:

-- Universe

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

-- Main

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

This works as intended. Of course, the Touch table isn’t moved over yet. That requires us to initialize it in Universe, and to forward the addTouched function from Main. Currently we have:

--Main

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

Which we’ll replace with:

function addTouched(anObject)
    U.universe:addTouched(anObject)
end

And in Universe:

Universe = class()

function Universe:init(x)
    self.Touched = {}
end

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

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

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

So the process was:

  • Forward Main’s touched to U.universe, as a method call, colon not dot.
  • In Universe init, add table Touched
  • Copy over Main’s functions touched and addTouched

I managed to screw this up, however, because I really don’t like the capitalized name Touch in Universe, so I changed it, and did so incorrectly. One thing at a time. We’ll make a note to deal with the names later.

It’s close to lunch time (Chet is here and you know how he is), but I am very tempted to start moving the Drawn table over as well. I’d like not to run out of time, so I’ll break for lunch with C het. Maybe I’ll come back today. More likely, tomorrow.

Anyway we have a running version with a start at Universe, and no weird bugs. This is better so far.


--# Universe
Universe = class()

function Universe:init(x)
    self.Touched = {}
end

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

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

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

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

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
    self.cannotCollide = true
    addTouched(self)
    U.add(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

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

Explosion = class()

function Explosion:init(ship)
    U.add(self)
    self.timeOut = 100
    self.pos = ship.pos
    self.cannotCollide = true
end

function Explosion:draw()
    self.timeOut = self.timeOut - 1
    if self.timeOut <= 0 then self:die() return end 
    pushMatrix()
    pushStyle()
    translate(self.pos:unpack())
    text("Blammo!")
    popStyle()
    popMatrix()
end

function Explosion:die()
    U.kill(self)
end

function Explosion:move()
    
end

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

-- S3 Spacewar

-- The Universe

U = {}
U.universe = Universe()
U.Tick = 0
U.Adds ={}
U.Drawn = {}
U.Removes = {}
U.Touched = {}
U.Running = true
U.MissileKillDistance = 20
U.kill = function(objToRemove)
    table.insert(U.Removes, objToRemove)
end
U.add = function(objToAdd)
    table.insert(U.Adds, objToAdd)
end

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

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

function draw()
    U.universe:draw()
end

function remove()
    for _, obj in ipairs(U.Removes) do
        U.Drawn[obj] = nil
    end
    U.Removes = {}
end

function add()
    for _, obj in ipairs(U.Adds) do
        U.Drawn[obj] = obj
    end
    U.Adds = {}
end

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

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

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

function addTouched(anObject)
    U.universe:addTouched(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.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

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

Missile = class()

Missile.count = 0

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

function Missile:move()
    self.distance = self.distance + self.vel:len()
    if self.distance > self.maxDistance then
        self:die()
        return
    end
    self.pos = 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:die()
    U.kill(self)
end


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

Ship = class()

    Ship.MaxMissiles = 50
    Ship.TicksBetweenMissiles = U.MissileKillDistance + 1
    Ship.ThrustAmount = vec2(0,0.02)
    Ship.TurnAmount = 1
    Ship.MaxSpeed = 3
-- do more of this

function Ship:init(shipNumber)
    self.name = string.format("Ship %d", shipNumber)
    self.missileLoad = Ship.MaxMissiles
    self.timeLastFired = -Ship.TicksBetweenMissiles
    self.shipNumber = shipNumber
    
    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)
    }
    U.add(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 =  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 = 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: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 > Ship.MaxSpeed then
        proposed = proposed*Ship.MaxSpeed/speed
    end
    return proposed
end

function Ship:die()
    U.kill(self)
    Explosion(self)
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: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
-- 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)
            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)
            collisions()
            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. See what I did there?