I thought I knew what we had to do today but I was a bit mistaken. We need more, and better thinking … and code.

There was an embarrassing defect in the last release. Attempts to fire a missile accessed an out of date notion, Ship.ang, resulting in an error and no missiles. So one mission this morning is to think about how that happened and what we can do about it.

It’s easy – and true – to say “it was just an oversight”. Certainly it was. But how do we fix our tendency to miss things from time to time? “Think better” isn’t very good advice: we’re thinking as best we can. So we’ll see if there are ways to program that allow for fewer oversights.

I’m sure some of you are thinking about strict typing right now. In a more strict language, the fact that ang wasn’t defined would have given us an error. We’ll talk about that when we get to the topic.

What I plan to do this morning is a further improvement to the Missile. When a missile is fired, its velocity is presently speed 2.0 in the direction the ship is pointing. That’s not quite right: the missile should get a velocity equal to its own firing velocity *plus the velocity of the ship”. So let’s take a look at that:

function Missile:init(ship)
    function die()
        self:die()
    end
    self.pos = ship.pos
    self.vel = vec2(MissileVelocity,0):rotate(ship.radians)
    Missiles[self] = self
    tween(3, self, {}, tween.easing.linear, die)
end

The self.vel line there is the one I had to fix last night, by changing the use of ang, which no longer exists, to radians. We need to add the velocity of the ship to that value. Conveniently, the ship knows its velocity, so we can do this:

    self.vel = vec2(MissileVelocity,0):rotate(ship.radians) + ship.velocity

That works as intended.

missiles track

Now that that little task is done, let’s commit “missiles track” and look upon our works and despair (just a little).

That Darn Oversight

When we added acceleration to the Ship, I made a mistake between degrees and radians. Codea doesn’t help much with this, not least because sometimes it uses degrees and sometimes radians. So we changed the member variable from ang to radians, hoping that would help us remember the data type.

The firing problem was that Missile, which looks to the Ship during creation, was referring to ang and I didn’t catch it. My question now is whether there is something we can do beyond “be smarter”.

I have two ideas. First, we could have some tests that check whether things are being computed correctly. Certainly if there had been a test for missile velocity, it would have failed. It’s a bit late to close that barn door but we might want to write a test or two to improve our testing habits. As I’ve said before, in a visual app like this game, I have difficulty seeing what tests to write, and it’s so easy just to run the program and see if a change works.

Unless you check acceleration but don’t ever fire a missile after adding acceleration. It was breakfast time, so I didn’t do even a bit of game play.

Since you can’t expect me to skip a meal, it might be fair to ask me to improve the automated tests.

My second idea is to find ways to improve the code to make oversights less likely. The best way to do that is to make the code simpler, and the second best way is to make it more consistent.

(I don’t really know if those are the best ways in the world. They’re the best ones I can think of right now. See previous remarks about being smarter.)

We have three moving objects just now, asteroids, the ship, and the missiles. Their moving code looks like this:

function moveAsteroid(asteroid)
    local step = Ratio*vec2(Vel,0):rotate(asteroid.angle)
    local pos = asteroid.pos + step
    asteroid.pos = vec2(keepInBounds(pos.x, WIDTH), keepInBounds(pos.y, HEIGHT))
end


function actualShipMove()
    if Button.go then
        local accel = vec2(0.015,0):rotate(Ship.radians)
        Ship.velocity = Ship.velocity + accel
        Ship.velocity = maximize(Ship.velocity, 3)
    end
    Ship.pos = Ship.pos + Ship.velocity
    Ship.pos = vec2(keepInBounds(Ship.pos.x, WIDTH), keepInBounds(Ship.pos.y, HEIGHT))
end


function Missile:move()
    self.pos = self.pos + Ratio*self.vel
    self.pos = vec2(keepInBounds(self.pos.x, WIDTH), keepInBounds(self.pos.y, HEIGHT))
end

These are doing roughly the same thing, but they don’t look as similar as they might. Furthermore … where is Ratio being used in the ship at all? Remember that Ratio compensates for processor speed, to keep game speed the same on different processors. I don’t see that happening for the ship.

Another oversight,you say, Jeffries? Been a lot of oversights lately, wouldn’t you say? What’s going to be done about these oversights, hmm?

What we have here, lords and ladies, is duplication. Not your simple these two lines are the same kind of duplication, but the more pernicious these lines are doing the same things in different ways kind of duplication.

What do we do with duplication? We remove it. We do that “same thing” in one and only one place. How do we get there? One really good way is to make the duplication more visible, so that the parts that are the same look the same, and the parts that are different are set off from the parts that are the same.

Common Elements

What do all our moving objects have in common?

  1. They all have a position in the world. It’s even named pos in all cases.
  2. They all clip their position to remain within screen boundaries.
  3. They all add a small amount, the step, to their position, on every move.
  4. The step should be adjusted by Ratio, to give them all the same speed across processors. (The ship seems not to do this at present.)
  5. Probably, all velocities should be limited, not necessarily to the same limit.

Let’s look at what they don’t have in common:

  1. The asteroids and missiles never change speed or direction: they just plod along at whatever pace they’re given.
  2. The ship does change speed: it can accelerate in any direction.

One way of restating the difference is that the acceleration of asteroids and missiles is zero, and that of the ship may not be.

This is suggesting to me that everyone’s motion might look like this:

adjustVelocityByAcceleration() -- ship only
adjustVelocityByLimit() -- ship and missile
adjustVelocityByRatio() -- everyone
addVelocityToPosition() -- everyone
keepPositionInBounds() -- everyone

Now that I look at that I think we can do better. I’ll leave that there because I did think it, and my job here is to show you how things really happen, not how some god of programming would do it.

Let’s go back to the things in common and work with them.

  1. Everyone has a step, an increment to position;
  2. Everyone adjusts that step by Ratio
  3. Everyone adds the step to position
  4. Everyone keeps the position in bounds.

There are other operations that are not shared, at least this: some people change the step, for some it is constant. If it’s constant, we should just use it, adjusting it not at all, for efficiency.

So, in principle, asteroids and missiles should know their step, which would be a vector embodying their velocity and direction, and including the application of Ratio. They just do steps 3 and 4, given step.

And the ship computes its step, adding in any acceleration, and limiting by maximum velocity, adjusting for Ratio … and then does steps 3 and 4.

And the missiles need to be sure to apply Ratio and maximum velocity (if they have it) when they set up their step.

And we need a shared name for the step: and it should be velocity.

We have a plan

Well, we have enough of a plan. I’m moving toward each of these three objects having a move method that adjusts by velocity and clamps values to the screen, and to have them all be identical.

Let’s start to make that happen. First, Asteroid, which looks like this:

function createAsteroid()
    local a = {}
    a.pos = vec2(math.random(WIDTH), math.random(HEIGHT))
    a.angle = math.random()*2*math.pi
    a.shape = Rocks[math.random(1,4)]
    a.scale = 16
    return a
end


function moveAsteroid(asteroid)
    local step = Ratio*vec2(Vel,0):rotate(asteroid.angle)
    local pos = asteroid.pos + step
    asteroid.pos = vec2(keepInBounds(pos.x, WIDTH), keepInBounds(pos.y, HEIGHT))
end

function keepInBounds(value, bound)
    return (value+bound)%bound
end

We want to compute step during creation. But wait, we can’t apply Ratio at creation time, it can vary during the run. So we don’t do just steps 3 and 4 from above, we do 2, 3, and 4. I’ll refactor move and create thus:

function moveAsteroid(asteroid)
    local pos = asteroid.pos + Ratio*asteroid.step
    asteroid.pos = vec2(keepInBounds(pos.x, WIDTH), keepInBounds(pos.y, HEIGHT))
end

function createAsteroid()
    local a = {}
    a.pos = vec2(math.random(WIDTH), math.random(HEIGHT))
    a.shape = Rocks[math.random(1,4)]
    a.scale = 16
    local angle = math.random()*2*math.pi
    a.step = Ratio*vec2(Vel,0):rotate(angle)
    return a
end

That works, by actual test. Commit “canonical asteroid motion”.

Now Missile, which looks like this:

function Missile:init(ship)
    function die()
        self:die()
    end
    self.pos = ship.pos
    self.vel = vec2(MissileVelocity,0):rotate(ship.radians) + ship.velocity
    Missiles[self] = self
    tween(3, self, {}, tween.easing.linear, die)
end


function Missile:move()
    self.pos = self.pos + Ratio*self.vel
    self.pos = vec2(keepInBounds(self.pos.x, WIDTH), keepInBounds(self.pos.y, HEIGHT))
end

This seems problematical, since missiles are objects and asteroids are not, but I’m going to let that slide for now. I think it’ll sort out.

I’ll rewrite the move to look more like the asteroids one:

function Missile:move()
    local pos = self.pos + Ratio*self.vel
    self.pos = vec2(keepInBounds(self.pos.x, WIDTH), keepInBounds(self.pos.y, HEIGHT))
end

Now in Missile we called it vel and in Asteroid it’s step. Let’s stick with step. I was thinking velocity but the idea here is that all the velocity calculations have been done and we just have a step to take. So I’ll modify Missile to have step instead of vel.

local MissileVelocity = 2.0

function Missile:init(ship)
    function die()
        self:die()
    end
    self.pos = ship.pos
    self.step = vec2(MissileVelocity,0):rotate(ship.radians) + ship.velocity
    Missiles[self] = self
    tween(3, self, {}, tween.easing.linear, die)
end

Oops:

function Missile:move()
    local pos = self.pos + Ratio*self.step
    self.pos = vec2(keepInBounds(pos.x, WIDTH), keepInBounds(pos.y, HEIGHT))
end

I need a refactoring browser. Forgot to remove the self. inside the keepInBounds.

These two functions are the same. They’re so much the same that I think I could use one of them to do the work of the other

Just for fun:

function Missile:move()
    --local pos = self.pos + Ratio*self.step
    --self.pos = vec2(keepInBounds(pos.x, WIDTH), keepInBounds(pos.y, HEIGHT))
    moveAsteroid(self)
end

That actually works. The moveAsteroid function doesn’t know it wasn’t given an asteroid, it just looks for and uses step and pos.

That’s evil, of course, and I’ll back that change out for now. Then commit: “canonical missile motion”.

Now let’s do the ship. We’re doing to have to be careful, because the missile is referring to ship.velocity and we plan to change that to step. Don’t let me forget that.

Here’s the ship now:

function createShip()
    Ship.pos = vec2(WIDTH, HEIGHT)/2
    Ship.radians = 0
    Ship.velocity = vec2(0,0)
end


function moveShip()
    if Button.left then Ship.radians = Ship.radians + rotationStep end
    if Button.right then Ship.radians = Ship.radians - rotationStep end
    if Button.fire then if not Ship.holdFire then fireMissile() end end
    if not Button.fire then Ship.holdFire = false end
    actualShipMove()
end

function actualShipMove()
    if Button.go then
        local accel = vec2(0.015,0):rotate(Ship.radians)
        Ship.velocity = Ship.velocity + accel
        Ship.velocity = maximize(Ship.velocity, 3)
    end
    Ship.pos = Ship.pos + Ship.velocity
    Ship.pos = vec2(keepInBounds(Ship.pos.x, WIDTH), keepInBounds(Ship.pos.y, HEIGHT))
end

I rather hate that I allowed that moveShip function to keep its name. Anyway we’re here for a different purpose. I’ll name my new little mover, oh, finallyMove I guess, and we’ll do like this:

function createShip()
    Ship.pos = vec2(WIDTH, HEIGHT)/2
    Ship.radians = 0
    Ship.step = vec2(0,0)
end


function actualShipMove()
    if Button.go then
        local accel = vec2(0.015,0):rotate(Ship.radians)
        Ship.step = Ship.step + accel
        Ship.step = maximize(Ship.step, 3)
    end
    finallyMove(Ship)
end

function finallyMove(ship)
    local pos = ship.pos + Ratio*ship.step
    Ship.pos = vec2(keepInBounds(pos.x, WIDTH), keepInBounds(pos.y, HEIGHT))    
end

That works, and I remembered to fix the missile also. But did I remember to put Ratio into all three functions?

function finallyMove(ship)
    local pos = ship.pos + Ratio*ship.step
    Ship.pos = vec2(keepInBounds(pos.x, WIDTH), keepInBounds(pos.y, HEIGHT))    
end


function moveAsteroid(asteroid)
    local pos = asteroid.pos + Ratio*asteroid.step
    asteroid.pos = vec2(keepInBounds(pos.x, WIDTH), keepInBounds(pos.y, HEIGHT))
end


function Missile:move()
    local pos = self.pos + Ratio*self.step
    self.pos = vec2(keepInBounds(pos.x, WIDTH), keepInBounds(pos.y, HEIGHT))
end

I did in fact remember. Yay, me. Maybe I can be smarter after all.

Now all three of these moving objects use the same code to move. They all point to a different object / table when the function runs, but that’s just fine.

Now we can consolidate that function if we care to. We could put a new copy of that function in Main, move the keep there as well, and use the new function for all three cases.

I don’t like that. It’s semi-OK for the asteroids and ship to consolidate that function, but it doesn’t consolidate well for the object-oriented missile to call a global function. I’m not even really happy about calling the keepInBounds that way.

What’s another way of making three different things use the same function?

This could get tricky. First, let’s commit “canonical ship motion”.

Consolidating movement

I was thinking of providing a general motion function, since now we’ve written it three times, and plugging it into each of our tables. But in talking about this on one of my Slacks just now, I got an idea I like better: a method on Universe that moves things.

We can think of it as moving things according to the laws of the universe. It could even apply speed limits if we wish. So I’ll add a method to Universe and use it.

Maybe we’ll like this, maybe we won’t. Either way is OK. Here it is for missiles:

function Universe:moveObject(anObject)
    local pos = anObject.pos + Ratio*anObject.step
    anObject.pos = vec2(keepInBounds(pos.x, WIDTH), keepInBounds(pos.y, HEIGHT))    
end

function Missile:move()
    U:moveObject(self)
end

Now we can clearly do ship and asteroid the same way, replacing their calls to their own moves with calls to this one:

function moveAsteroid(asteroid)
    U:moveObject(asteroid)
end

function finallyMove(ship)
    U:moveObject(ship)
end

This works just fine. The question is whether we like it.

The good news is we have completely removed that duplication, hopefully preventing any further mistakes like leaving out Ratio or referring to non-existent variables.

The bad news is, now the universe moves all the things, instead of the things moving themselves. If I could have my druthers, I druther have objects moving themselves … all in the same way.

Is there a way to avoid the duplication but not cause the Universe to move all the objects … and do we even mind that it does move them?

For now, I think we’ll leave it this way. However, I think we should move the keepInBounds function to the Universe as well, since it should only be used right here.

function Universe:keepInBounds(value, bound)
    return (value+bound)%bound
end

That breaks a unit test, which was counting on keepInBounds being global. Fixed:

        _:test("Bounds function", function()
            _:expect(U:keepInBounds(100,1000)).is(100)
            _:expect(U:keepInBounds(1000,1000)).is(0)
            _:expect(U:keepInBounds(1001,1000)).is(1)
            _:expect(U:keepInBounds(-1,1000)).is(999)
        end)

Commit “universe moves things”.

Wider View

Let’s think about this change. Taken overall, it’s pretty global, ultimately affecting five tabs, Test, Ship, Asteroid, Missile, and Universe. But we did it in small steps and rarely broke anything.

The errors I made were, I believe, mostly failures to catch all the occurrences of something that needed to change because its name or access had changed, Failing to add in a “self” where needed, or referencing an old variable name.

Oversights, in other words. I hope that this centralization will prevent further such oversights in this one area, but there are plenty of other areas to be concerned about, like the approximately 500 lines of code that are not moving the object.

I think this is a bit better, a bit more like it “should” be, but I’m not certain. I’m not entirely comfortable having motion done “to” an object rather than “by” the object. It makes a kind of sense, but it seems that it reduces their autonomy.

I wouldn’t do this trick to save memory by removing the duplication. I did it because it ensures that everyone who should do the same thing does in fact do the same thing. It’s a bit odd but for now, I’ll pay the price of oddness for the improvement in consistency.

I look forward to your comments (via Twitter or other means) and hope to see you next time!


--# Main
-- Asteroids
-- RJ 20200511

Touches = {}
Ratio = 1.0 -- draw time scaling ratio
Score = 0

function setup()
    U = Universe()
    U:createAsteroids()
    Score = 0
    --displayMode(FULLSCREEN_NO_BUTTONS)
    createButtons()
    createShip()
end

function draw()
    Ratio = DeltaTime/0.0083333
    --displayMode(FULLSCREEN_NO_BUTTONS)
    checkButtons()
    pushStyle()
    background(40, 40, 50)
    U:draw()
    drawButtons()
    drawShip()
    moveShip()
    drawMissiles()
    drawSplats()
    drawScore()
    popStyle()
    U:findCollisions()
end

function drawScore()
    local s= "000000"..tostring(Score)
    s = string.sub(s,-5)
    fontSize(100)
    text(s, 200, HEIGHT-60)
end

function touched(touch)
    if touch.state == ENDED or touch.state == CANCELLED then
        Touches[touch.id] = nil
    else
        Touches[touch.id] = touch
    end
end

--# TestAsteroids
-- TestAsteroids
-- RJ 20200511
    
function testAsteroids()
    CodeaUnit.detailed = true

    _:describe("Asteroids First Tests", function()

        _:before(function()
            -- Some setup
        end)

        _:after(function()
            -- Some teardown
        end)
        
        _:test("Hookup", function()
            _:expect( 2+1 ).is(3)
        end)
        
        _:test("Random", function()
            local min = 100
            local max = 0
            for i = 0,1000 do
                local rand = math.random()*2*math.pi
                if rand < min then min = rand end
                if rand > max then max = rand end
            end
            _:expect(min < 0.01).is(true)
            _:expect(max > 6.2).is(true)
        end)
        
        _:test("Rotated Length", function()
            for i = 0, 1000 do
                local rand = math.random()*2*math.pi
                local v = vec2(1.5,0):rotate(rand)
                local d = v:len()
                _:expect(d > 1.495).is(true)
                _:expect(d < 1.505).is(true)
            end
        end)
        
        _:test("Some rotates go down", function()
            local angle = math.rad(-45)
            local v = vec2(1,0):rotate(angle)
            local rvx = v.x*1000//1
            local rvy = v.y*1000//1
            _:expect(rvx).is(707)
            _:expect(rvy).is(-708)
        end)
        
        _:test("Bounds function", function()
            _:expect(U:keepInBounds(100,1000)).is(100)
            _:expect(U:keepInBounds(1000,1000)).is(0)
            _:expect(U:keepInBounds(1001,1000)).is(1)
            _:expect(U:keepInBounds(-1,1000)).is(999)
        end)

    end)
end

--# Shapes
RR1 = {
  vec4(0.000000, 2.000000, 2.000000, 4.000000),
  vec4(2.000000, 4.000000, 4.000000, 2.000000),
  vec4(4.000000, 2.000000, 3.000000, 0.000000),
  vec4(3.000000, 0.000000, 4.000000, -2.000000),
  vec4(4.000000, -2.000000, 1.000000, -4.000000),
  vec4(1.000000, -4.000000, -2.000000, -4.000000),
  vec4(-2.000000, -4.000000, -4.000000, -2.000000),
  vec4(-4.000000, -2.000000, -4.000000, 2.000000),
  vec4(-4.000000, 2.000000, -2.000000, 4.000000),
  vec4(-2.000000, 4.000000, 0.000000, 2.000000)
}

RR2 = {
  vec4(2.000000, 1.000000, 4.000000, 2.000000),
  vec4(4.000000, 2.000000, 2.000000, 4.000000),
  vec4(2.000000, 4.000000, 0.000000, 3.000000),
  vec4(0.000000, 3.000000, -2.000000, 4.000000),
  vec4(-2.000000, 4.000000, -4.000000, 2.000000),
  vec4(-4.000000, 2.000000, -3.000000, 0.000000),
  vec4(-3.000000, 0.000000, -4.000000, -2.000000),
  vec4(-4.000000, -2.000000, -2.000000, -4.000000),
  vec4(-2.000000, -4.000000, -1.000000, -3.000000),
  vec4(-1.000000, -3.000000, 2.000000, -4.000000),
  vec4(2.000000, -4.000000, 4.000000, -1.000000),
  vec4(4.000000, -1.000000, 2.000000, 1.000000)
}

RR3 = {
  vec4(-2.000000, 0.000000, -4.000000, -1.000000),
  vec4(-4.000000, -1.000000, -2.000000, -4.000000),
  vec4(-2.000000, -4.000000, 0.000000, -1.000000),
  vec4(0.000000, -1.000000, 0.000000, -4.000000),
  vec4(0.000000, -4.000000, 2.000000, -4.000000),
  vec4(2.000000, -4.000000, 4.000000, -1.000000),
  vec4(4.000000, -1.000000, 4.000000, 1.000000),
  vec4(4.000000, 1.000000, 2.000000, 4.000000),
  vec4(2.000000, 4.000000, -1.000000, 4.000000),
  vec4(-1.000000, 4.000000, -4.000000, 1.000000),
  vec4(-4.000000, 1.000000, -2.000000, 0.000000)
}

RR4 = {
  vec4(1.000000, 0.000000, 4.000000, 1.000000),
  vec4(4.000000, 1.000000, 4.000000, 2.000000),
  vec4(4.000000, 2.000000, 1.000000, 4.000000),
  vec4(1.000000, 4.000000, -2.000000, 4.000000),
  vec4(-2.000000, 4.000000, -1.000000, 2.000000),
  vec4(-1.000000, 2.000000, -4.000000, 2.000000),
  vec4(-4.000000, 2.000000, -4.000000, -1.000000),
  vec4(-4.000000, -1.000000, -2.000000, -4.000000),
  vec4(-2.000000, -4.000000, 1.000000, -3.000000),
  vec4(1.000000, -3.000000, 2.000000, -4.000000),
  vec4(2.000000, -4.000000, 4.000000, -2.000000),
  vec4(4.000000, -2.000000, 1.000000, 0.000000)
}

Rocks = {RR1,RR2,RR3,RR4}
--# Ship
-- Ship
-- RJ 20200520

local Ship = {}

local rotationStep = math.rad(1) -- one degree in radians

function createShip()
    Ship.pos = vec2(WIDTH, HEIGHT)/2
    Ship.radians = 0
    Ship.step = vec2(0,0)
end

function drawShip()
    local sx = 10
    local sy = 6
    pushStyle()
    pushMatrix()
    translate(Ship.pos.x, Ship.pos.y)
    rotate(math.deg(Ship.radians))
    strokeWidth(2)
    stroke(255)
    line(sx,0, -sx,sy)
    line(-sx,sy, -sx,-sy)
    line(-sx,-sy, sx,0)
    popMatrix()
    popStyle()
end

function moveShip()
    if Button.left then Ship.radians = Ship.radians + rotationStep end
    if Button.right then Ship.radians = Ship.radians - rotationStep end
    if Button.fire then if not Ship.holdFire then fireMissile() end end
    if not Button.fire then Ship.holdFire = false end
    actualShipMove()
end

function actualShipMove()
    if Button.go then
        local accel = vec2(0.015,0):rotate(Ship.radians)
        Ship.step = Ship.step + accel
        Ship.step = maximize(Ship.step, 3)
    end
    finallyMove(Ship)
end

function finallyMove(ship)
    U:moveObject(ship)
end

function maximize(vec, size)
    local s = vec:len()
    if s <= size then
        return vec
    else
        return vec*size/s
    end
end

function fireMissile()
    Ship.holdFire = true
    Missile(Ship)
end

--# Button
-- Button
-- RJ 20200520

Button = {}
local Buttons = {}

function createButtons()
    local dx=50
    local dy=200
    table.insert(Buttons, {x=dx, y=dy, name="left"})
    table.insert(Buttons, {x=dy, y=dx, name="right"})
    table.insert(Buttons, {x=WIDTH-dx, y=dy, name="fire"})
    table.insert(Buttons, {x=WIDTH-dy, y=dx, name = "go"})
end

function checkButtons()
    Button.left = false
    Button.right = false
    Button.go = false
    Button.fire = false
    for id,touch in pairs(Touches) do
        for i,button in ipairs(Buttons) do
            if touch.pos:dist(vec2(button.x,button.y)) < 50 then
                Button[button.name]=true
            end
        end
    end
end

function drawButtons()
    pushStyle()
    ellipseMode(RADIUS)
    textMode(CENTER)
    stroke(255)
    strokeWidth(1)
    for i,b in ipairs(Buttons) do
        pushMatrix()
        pushStyle()
        translate(b.x,b.y)
        if Button[b.name] then
            fill(128,0,0)
        else
            fill(128,128,128,128)
        end
        ellipse(0,0, 50)
        fill(255)
        fontSize(30)
        text(b.name,0,0)
        popStyle()
        popMatrix()
    end
    popStyle()
end

--# Asteroid
-- Asteroid
-- RJ 20200520

local DeadAsteroids = {}
local Vel = 1.5

function createAsteroids(asteroids)
    for i = 1,4 do
        local a = createAsteroid()
        asteroids[a] = a
    end
end

function createAsteroid()
    local a = {}
    a.pos = vec2(math.random(WIDTH), math.random(HEIGHT))
    a.shape = Rocks[math.random(1,4)]
    a.scale = 16
    local angle = math.random()*2*math.pi
    a.step = Ratio*vec2(Vel,0):rotate(angle)
    return a
end

function drawAsteroids(asteroids)
    pushStyle()
    stroke(255)
    fill(0,0,0, 0)
    strokeWidth(2)
    rectMode(CENTER)
    for i,asteroid in pairs(asteroids) do
        drawAsteroid(asteroid)
        moveAsteroid(asteroid)
    end
    popStyle()
    killDeadAsteroids(asteroids)
end

function killDist(asteroid)
    local s = asteroid.scale
    if s == 16 then return 64 elseif s == 8 then return 32 else return 16 end
end

function killDeadAsteroids(asteroids)
    for k,a in pairs(DeadAsteroids) do
        asteroids[a] = nil
    end
    DeadAsteroids = {}
end

function deathSize()
    local i = 0
    for k, a in pairs(DeadAsteroids) do
        i = i + 1
    end
    return i
end

function scoreAsteroid(asteroid)
    local s = asteroid.scale
    local inc = 0
    if s == 16 then inc = 20
    elseif s == 8 then inc = 50
    else inc = 100
    end
    Score = Score + inc
end

function splitAsteroid(asteroid, asteroids)
    if asteroid.scale == 4 then
        Splat(asteroid.pos)
        DeadAsteroids[asteroid] = asteroid
        return
    end
    asteroid.scale = asteroid.scale//2
    asteroid.angle = math.random()*2*math.pi
    local new = createAsteroid()
    new.pos = asteroid.pos
    new.scale = asteroid.scale
    asteroids[new] = new
    Splat(asteroid.pos)
end

function drawAsteroid(asteroid)
    pushMatrix()
    pushStyle()
    translate(asteroid.pos.x, asteroid.pos.y)
    ellipse(0,0,2*killDist(asteroid))
    scale(asteroid.scale)
    strokeWidth(1/asteroid.scale)
    for i,l in ipairs(asteroid.shape) do
        line(l.x, l.y, l.z, l.w)
    end
    popStyle()
    popMatrix()
end

function moveAsteroid(asteroid)
    U:moveObject(asteroid)
end


--# Splat
-- Splat
-- RJ 20200521

local Splats = {}

local Vecs = {
vec2(-2,0), vec2(-2,-2), vec2(2,-2), vec2(3,1), vec2(2,-1), vec2(0,2), vec2(1,3), vec2(-1,3), vec2(-4,-1), vec2(-3,1)
}

function drawSplats()
    for k, splat in pairs(Splats) do
        splat:draw()
    end
end

Splat = class()

function Splat:init(pos)
    local die = function()
        Splats[self] = nil
    end
    self.pos = pos
    Splats[self] = self
    self.size = 2
    self.diameter = 6
    self.rot = math.random(0,359)
    tween(4, self, {size=10, diameter=1}, tween.easing.linear, die)
end

function Splat:draw()
    pushStyle()
    pushMatrix()
    translate(self.pos.x, self.pos.y)
    fill(255)
    stroke(255)
    rotate(self.rot)
    local s = self.size
    for i,v in ipairs(Vecs) do
        ellipse(s*v.x, s*v.y, self.diameter)
    end
    popMatrix()
    popStyle()
end

--# Missile
-- Missile
-- RJ 20200522

Missiles = {}

function drawMissiles()
    pushStyle()
    pushMatrix()
    fill(255)
    stroke(255)
    for k, missile in pairs(Missiles) do
        missile:draw()
    end
    popMatrix()
    popStyle()
    for k, missile in pairs(Missiles) do
        missile:move()
    end
end

Missile = class()

local MissileVelocity = 2.0

function Missile:init(ship)
    function die()
        self:die()
    end
    self.pos = ship.pos
    self.step = vec2(MissileVelocity,0):rotate(ship.radians) + ship.step
    Missiles[self] = self
    tween(3, self, {}, tween.easing.linear, die)
end

function Missile:die()
    Missiles[self] = nil
end

function Missile:draw()
    ellipse(self.pos.x, self.pos.y, 6)
end

function Missile:move()
    U:moveObject(self)
end

--# Universe
-- Universe
-- RJ 20200523

Universe = class()

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

function Universe:draw()
    drawAsteroids(self.asteroids)
end

function Universe:createAsteroids()
    createAsteroids(self.asteroids)
end

function Universe:findCollisions()
    for i,a in pairs(self.asteroids) do
        for k,m in pairs(Missiles) do
            if m.pos:dist(a.pos) < killDist(a) then
                scoreAsteroid(a)
                splitAsteroid(a, self.asteroids)
                m:die()
            end
        end
    end
end

function Universe:moveObject(anObject)
    local pos = anObject.pos + Ratio*anObject.step
    anObject.pos = vec2(self:keepInBounds(pos.x, WIDTH), self:keepInBounds(pos.y, HEIGHT))    
end

function Universe:keepInBounds(value, bound)
    return (value+bound)%bound
end