Tozier and I began by playing the game a bit. In the course of that we identified several things we’d like to change:

  • Thrust should be visible on screen
  • Hard to tell the ships apart
  • The sun is just a dot and should be more interesting
  • Game should restart when someone dies
  • No rapid fire of bullets because the ship’s bullets kill each other
  • Ship rotation is too slow for easy steering
  • Codea start/pause controls should be offscreen during play
  • But we do need to take pictures and videos
  • Ship max speed is probably too low
  • Gravity is probably too high
  • Thrust is probably too low
  • Should bullets be affected by gravity or not?
  • We have hit points but no display of damage status on a hit

Looking ahead, we think the game should have a “control panel” of parameters that can be adjusted before the game. Then you’d push “Run”, the panel would go away, and the game would commence. The Codea controls would hide at that time. Some trick, like touching the Sun, would bring the Codea controls back so you can restart the game, start a video, whatever.

We did a Spike. Well, actually, we coded something and declared it a Spike after a while. Note to self: whenever coding something, say at the beginning “Let’s try a Spike”, so you won’t be embarrassed later about throwing it away. (No, seriously: one should never be embarrassed to throw away something that drifts off in the wrong direction.)

What did we learn from our Spike? We built a “Run” button and tried a couple of ways of making it work. We gave the Universe a “Running” state and, on the second try, made it clear the screen in a different place so that the screen is black when not running. Along the way we learned that Codea has two display buffers that it switches between. We had an interesting effect, which was that Codea does not clear those buffers before giving them to you, so when the game stops running, the last two frames swap back and forth with an interesting flicker effect. So we moved a few things around in the draw logic until we got a black screen when not running.

We found that ownership of universe creation and ownership of the screen is a bit entangled. We tried a button to change the Ship’s maximum speed, which we did by setting the existing Ship class variable to a different value. This (it seems obvious now) affects any new ships, if the Universe and Ships are created anew, but does not affect Ships that already exist. Since Main was building the universe and installing the ships, this surprised us a bit. The creation of the Universe may need to be put into the Universe.

So as we go forward we want to be alert to the ownership of system state and see if we want to improve it. For example, when ships apply thrust, they apply Ship.ThrustAmount. But when they check maximum velocity self.MaxSpeed is used. But that’s the MaxSpeed copied into the ship instance from the class, not the class variable. So something is inconsistent there.

All these are things we’ll take into account now that our Spike is over.

One more thing: since we changed the buttons, our button tests are not running. We didn’t even look at this but we should do so and decide what to do about it.

An interesting day. We reverted the code to what it was before we started. Yay, Codea Source Code Management and yay, Github.

A New Day Dawns

Just to loosen up, I add a Running button to the game:

    parameter.boolean("Running", false)

And to make it work, in Universe:

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

It took two tries to get this just right: we need to updateUniverse to pick up any added objects, like the sun and ships, but not to move them about or let them explode if they haven’t already done so. The result looks like this:

IMAGE HERE

We decide, somehow, to work with the screen layout. We want three states:

  1. When the game starts, the Codea controls are visible and the game is not running;
  2. When the game is set to running, the Codea controls, and the run/restart controls all go away;
  3. When the sun is touched, we go back to showing the controls.

Research tells us that we want the screen settings to be:

  1. STANDARD
  2. FULLSCREEN_NO_BUTTONS
  3. STANDARD

Our first attempt at this teaches us a lesson. We add this to our parameter:

    parameter.boolean("Running", false, function()
        if Running then displayMode(FULLSCREEN) end
    end)

Which does switch us to the full screen mode but doesn’t relocate the high-end buttons:

IMAGE HERE

It seems to me that this is because the buttons’ positions are computed when they are built, and need to be computed more dynamically. At a second glance we note that the ships’ positions, and the Sun’s position are also not computed. One possibility is to calculate all those offsets dynamically. Another is to restart the game with the new HEIGHT and WIDTH. I am inclined toward the latter. This will mean that our Running button starts the game over but I think we can live with that. We could have a separate pause button someday if we need it.

Our next effort, shown above, takes a few tries and I don’t like everything about what we found. We knew that we needed to touch the sun (as one does) to get the screen back. We discovered, if we didn’t know it, that we want to restart the program at that point. So in Sun we added this touch:

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)) < 50
end

This worked, after we remembered that Sun had to register as touchable. However, calling setup() didn’t really stop the game, because the Universe is created outside setup, basically at compile time. We moved creation inside setup, only to realize that the Universe is relied upon by other classes, who also do things at compile time. Meh. Our “fix” was to create a new universe inside setup, leaving the compile-time creation there as well:

function setup()
    U = Universe()
    displayMode(STANDARD)
    parameter.action("Automate", function()
        automate()
    end)
    parameter.action("Running", function()
        displayMode(FULLSCREEN_NO_BUTTONS)
        newGame()
    end)
end

But in talking about it with you, we realize that initializing the universe should suffice: we don’t need to make a new one:

function setup()
    U:init()
    displayMode(STANDARD)
    parameter.action("Automate", function()
        automate()
    end)
    parameter.action("Running", function()
        displayMode(FULLSCREEN_NO_BUTTONS)
        newGame()
    end)
end

We can’t show you a movie of this, because Codea won’t take a movie of the buttons appearing and disappearing but it works as intended. Time to push the code.

We also noticed that the ships seem to be controlled by the wrong buttons. That is, I expect that my ship should be the one that faces away from my buttons at startup. The reverse is true now. Presumably we can change the button setup to reverse who gets which buttons:

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 == 1 then
        return Button(x,y, w2, h2)
    else
        return Button(WIDTH-x,HEIGHT-y, w2, h2)
    end
end

I’m guessing changing that to say if self.shipNumber == 2 will fix this issue. And in fact that works. Now we are in a position to do lots of the things on our list. We push the code again, to continue another day. Here’s where we are now:


--# 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, 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)) < 50
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(anObject, vel)
    local speed = vel:len()
    local max = anObject.MaxSpeed
    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()
    U:init()
    displayMode(STANDARD)
    parameter.action("Automate", function()
        automate()
    end)
    parameter.action("Running", function()
        displayMode(FULLSCREEN_NO_BUTTONS)
        newGame()
    end)
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()
    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.ThrustAmount = vec2(0,0.02)
    Ship.TurnAmount = 1
    Ship.MaxSpeed = 3
    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 =  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: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))
    proposed = proposed + U:gravity(self)
    return U:limitSpeed(self, 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