Some parameters

Here are some parameters we’d like to tune:

  • Ship rotation is too slow for easy steering
  • Ship max speed is probably too low
  • Gravity is probably too high
  • Thrust is probably too low
  • No rapid fire of bullets because the ship’s bullets kill each other

One possible question is whether there are some we’ll set and forget, removing their controls. For now, let’s assume we’ll leave them all, so there is no particular reason to solve one before another.

Tozier raises the issue of whether and how we might save preferred parameters (and sets) from game to game. Research …

It turns out that Codea has a number of levels of persistent data storage, local, project, and global. Local belongs to this instance of this program: Spacewar on my machine. Project belongs to the project: Spacewar on all our machines. Global belongs to a given machine: all project on my machine. There is also the ability to write any text to a project tab in the editor, and there are JSON encode and decode functions.

The local, project, and global storage amount to key-value pairs. We figure we’ll start with putting things in local storage.

Looking further ahead than we have a right to, I imagine that given code like this:

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

We might see the setting of MissileKillDistance first checking local storage and using that value if any, otherwise the coded-in default. But we’ll see when we get there.

Let’s pick an easy one to do: we agree on ship maximum speed:

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

First step is to make a MaxSpeed parameter and make it work. We’ll save it in local storage as part of this effort. One thing at a time.

It’s clear that it matters when we look at the parameter. If we are happy only setting it between games, that’s one thing. If we want to tweak it in-game, that’s another. As we’re set above, new ships get whatever value in in the Ship class. Tozier is contemplating school zones in space where you have to slow down. I am ignoring him.

-- Main


    parameter.number("VMax", 2, 20)
    VMax = 20

This gives us a parameter and maxes it out. The result looks like this:

IMAGE HERE

MOVIE HERE

So this is a bit of a hack, because we did this in Ship:

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, proposed)
end

This was needed because Universe checks the speed limit, as you’d imagine it might but why?

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

It’s nice to have this function at some global level like Universe. It’s not nice that it looks inside the object and rips out information: the object should pass in the max speed, not itself. We have discovered yet another little design flaw. Right now we’ll make a note of it because we’re putting in the tuning. However, fixing this may be the right way to do tuning, at the cost of a global refactoring of everyone who calls this function. We’ll let Codea’s replace guide us. Turns out only Ship and Fragment do this. An easy fix, so we’ll refactor the calling sequence.

This is not a violation of the “one-hat” rule, because we are green as regards our parameter.

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

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

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

This works, and now the Ship is using VMax as its top speed. We can refactor out that assignment into self.MaxSpeed if we wish. I think we need to think a bit more clearly about the relationship between the objects, the classes, and the parameters.

We’ve learned that VMax of 20 is too high, and 10 probably is not. We noticed that it takes too long to get to that speed, so we’ll need to tune thrust real soon now. We’re not entirely happy with the Ship querying these global parameters right in the middle of everything. But … what would be better? We discuss the local storage, which we believe will get read when the game starts (what do we mean by when the game starts: first execution from scratch start, not just New Game if we build that). REPHRASE THIS.

The idea is that we’ll wind up with these values pretty well tuned so that it will be rare for anyone to be tweaking them. Thus, store them.

But still, how and when should an object access these parameters? We could read these values upon Ship creation, for example, somewhere in here:

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

In here, we’d fetch the Ship’s VMax and store it in self.MaxSpeed. This allows my ship to be faster than yours if I can come up with a magic incantation. From whence do we fetch VMax? When does it come from the local storage, when from the Parameter? It must come from the Parameter, since otherwise there’s no useful way to test other speeds. So somewhere in BigInit we will read the local store and save it into the Parameter.

We probably need a save button to save current parm values into local storage. Let’s do that now. And we do and it works. We check that by starting the program, and VMax comes up 2, and we slide it to some other number (13), and we press our save button and restart the game and sure enough VMax comes up 13 next time.


--# 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)) < 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(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()
    U:init()
    displayMode(STANDARD)
    parameter.action("Automate", function()
        automate()
    end)
    parameter.action("Run", function()
        displayMode(FULLSCREEN_NO_BUTTONS)
        newGame()
    end)
    parameter.integer("VMax", 2, 20)
    parameter.action("Save", function()
        saveLocalData("VMax", VMax)
    end)
    VMax = readLocalData("VMax")
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)
    self.MaxSpeed = VMax
    return U:limitSpeed(self.MaxSpeed, 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