I think today we’ll do waves of asteroids. And some tests.

Today’s Code

I watched some old Asteroids videos and have determined at least two useful things. First, when you destroy all the asteroids, a new wave starts with your ship wherever it was. Second, when your ship is destroyed, it returns in the middle of the screen, and the asteroids just keep on going. If one is about to hit the middle, too bad for you.

Actually, now that my brain is running (it’s 7 AM by the way), I recall some other facts. Waves are of size 4, 6, 8, 10, and 11. After 11 they just continue at 11. I think that was a 6502 capacity issue.

Asteroids always start at the screen edge at the beginning of a wave. (Ours are random on the screen at present.)

I’ve not sussed this out entirely, but it is clear that asteroid fragments generally move faster than the original (conservation of momentum? seems unlikely.) And they don’t start right at the position of the larger one, but offset from it. This will require more study of the 6502 code.

I think we’ll address most of what’s above, starting today. An additional observation that I think I’ve mentioned before is that our ship shape isn’t the same as the original. We’ll improve that one of these days.

And finally: there have been what, two or maybe even three defects discovered by my friends over on the Codea forum. That just won’t do. I do not accept that defects are inevitable in shipped products, even in the increments.

Mind you, I’m no fool. Well, maybe I am, but I’m not foolish enough to believe there will never be a defect, and certainly as I write these articles you’ve seen me make many mistakes of various kinds. But I’ve been programming for over a half-century, and what I’ve observed in the past two-plus decades of Agile Software Development is that teams can reduce their shipped defects by a factor of 100 or more. The two main ways they accomplish this is by working more closely together, and by microtesting in the TDD style.

For folks new to the idea, TDD, or Test-Driven Development, is a style of programming where, before you start putting in the code to do some new thing, you write a small, automated test, which my colleague GeePaw Hill calls a “microtest”. You run the test, seeing that it fails. You implement just enough code to make the test pass, rinse, repeat.

This sounds like it would be truly dreadful, especially if you think you hate testing. And it’s not trivial to get good at TDD, either. But once you’re in the groove, you just trip along adding tests and working code … because all the code you write is tested. For those of us who have practiced this skill, it’s really quite pleasant to do.

Then why am I not doing it here? The reason is that for microtesting to feel good, we have to be able to quickly write the next test. If testing is difficult, it doesn’t feel good and if it doesn’t feel good, we don’t do it.

And I do not usually see how to do decent microtests on graphical programs like this one, so my tests have so far been too few to mention.

However, the defects have been, while also few, more than I care to ship. I’d like each day’s release of this code to work exactly as intended. It might be short of a feature, or the BLAMMO might stay on the screen forever, but it would be that way because I decided it would be that way, not a mistake.

So I’m going to try to write more tests. I’m not optimistic that I’ll figure out how to make it go smoothly. But I’ll try, and in trying, I’ll learn something.

OK, let’s get to work.

Edges

Let’s start by changing our asteroid creation so that the asteroids all start out on a screen edge. I think we don’t need to care about their direction. If they start at the top going up, it’ll be a moment away from having started at the bottom.

We’d like them to be equally distributed around the four edges, and for their direction to continue to be random.

Hm. This is kind of an interesting constraint. Each asteroid will have one coordinate that is either, say, zero or screen size minus one, and the other coordinate random on the length of the other axis.

And how might we test some or all of that requirement?

Let’s think about how we might implement this requirement simply. My first sensible thought is this:

  • first randomize both X and Y.
  • then select one of four random setters, x = 0, x = WIDTH-1, y = 0, y = HEIGHT-1 and jam that value into the the result.

That seems weird. Maybe I was wrong about sensible. Let’s see. We only have four cases: <0,random>, <WIDTH, random>, <random, 0>, <random, HEIGHT>. Let’s just build four functions and call one randomly.

That might be good.

How to test that? I’m not at all sure. Testing random stuff is hard. Anyway, first I want to spike this code and see what it would look like. That’s allowed, isn’t it?

Spike

What about this:

function Asteroid:edgeCoordinate()
    local r = math.random
    local left = function()
        return vec2(0,r(HEIGHT))
    end
    local right = function()
        return vec2(WIDTH, r(HEIGHT))
    end
    local top = function()
        return vec2(r(WIDTH,HEIGHT))
    end
    local bottom = function()
        return vec2(rWIDTH), 0))
    end
    local tab = {right, left, top, bottom}
    return tab[r(1,4)]()
end

I can sort of write a test for that:

        _:test("edge coordinates", function()
            for i = 0,1000 do
                local v = Asteroid:edgeCoordinate()
                local ok = v.x==0 or v.y==0 or v.x==WIDTH or v.y == HEIGHT
                assert(ok).is(true)
            end
        end)

That’s not entirely satisfying, as it loops 1000 times but it’ll be quite fast. Does it run?

Well, no, it says “attempt to index a boolean value”. I don’t know whether that’s complaining about something in the test or in edgeCoordinate. I’m not even really sure if it’s legit to call edgeCoordinate as a class method like that.

One thing at a time. If I set ok to true in the test, I get that message. So CodeaUnit isn’t OK testing booleans, I guess. I’ll explore that later.

Fool! Look what I wrote: assert That’s Ruby thinking. We need expect:

        _:test("edge coordinates", function()
            for i = 0,1000 do
                local v = Asteroid:edgeCoordinate()
                local ok = v.x==0 or v.y==0 or v.x==WIDTH or v.y == HEIGHT
                _:expect(ok).is(true)
            end
        end)

Let me just point out that most authors would just go back and edit the past and put in the correct test. I want you to see that everyone does dumb things. Well, to see that I do, anyway.

So now we have an error at line 80 in Asteroid tab, “bad argument to ‘r’, interval is empty. Let’s see what that is about. Here are lines 79-81:

    local top = function()
        return vec2(r(WIDTH,HEIGHT))
    end

The parens are in the wrong place.

    local top = function()
        return vec2(r(WIDTH),HEIGHT)
    end

And voila! the edge coordinates have been tested 1000 times and always had a coordinate of 0 or screen size. Are we totally confident now? Probably not, but we have a reasonable belief that it’s working. I’m going to plug it in. Asteroid:init looks like this:

function Asteroid:init()
    self.pos = vec2(math.random(WIDTH), math.random(HEIGHT))
    self.shape = Rocks[math.random(1,4)]
    self.scale = 16
    local angle = math.random()*2*math.pi
    self.step = vec2(Vel,0):rotate(angle)
end

Changed to …

function Asteroid:init()
    self.pos = self:edgeCoordinate()
    self.shape = Rocks[math.random(1,4)]
    self.scale = 16
    local angle = math.random()*2*math.pi
    self.step = vec2(Vel,0):rotate(angle)
end

And it works. And I don’t like it. With the values selected randomly, it can happen that all the asteroids start on one side and that looks weird. I think I’ll make it cycle, using the index of the asteroid we’re creating. This does mean I need to change the unit test, because now the function’s not doing to be random:

… ten minutes pass …

Darn! This isn’t going to work as I intended. My plan was to call edgeCoordinate in the asteroid creation, passing in the index of the one we’re creating. That was slightly messy already because as the indexes increase we need to mod them and i mod 4 is in 0-3, not 1-4, so fudge factor. But then splitAsteroid wants to set its own position.

Well, maybe its OK. I’ll just pass in a value and override it:

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 = Asteroid(1)
    new.pos = asteroid.pos
    new.scale = asteroid.scale
    asteroids[new] = new
    Splat(asteroid.pos)
end

Meanwhile my test actually works:

        _:test("edge coordinates", function()
            _:expect(Asteroid:edgeCoordinate(5).x).is(WIDTH)
            _:expect(Asteroid:edgeCoordinate(6).x).is(0)
            _:expect(Asteroid:edgeCoordinate(7).y).is(HEIGHT)
            _:expect(Asteroid:edgeCoordinate(8).y).is(0)
        end)

Note that I gave it coordinates above 4 to make sure the modulus stuff was working:

function Asteroid:edgeCoordinate(index)
    index = index%4
    if index == 0 then index = 4 end
    local r = math.random
    local left = function()
        return vec2(0,r(HEIGHT))
    end
    local right = function()
        return vec2(WIDTH, r(HEIGHT))
    end
    local top = function()
        return vec2(r(WIDTH),HEIGHT)
    end
    local bottom = function()
        return vec2(r(WIDTH), 0)
    end
    local tab = {right, left, top, bottom}
    return tab[index]()
end

That’s simple but a bit nasty. Shouldn’t matter, we can clean it if we wish. Does the game run?

edges

It does, but I’m still not entirely happy. You’d kind of like the asteroids to come falling inward from all four edges, and half the time they come in pairs from top or bottom. They’re rezzing correctly, but then their random direction means they might both go roughly the same way. So I don’t love the look of that.

I have the feeling that the right thing to do is revert and start over. I can rationalize that the edge code is moving in the right direction, and so on, but my gut tells me it’s not going well. When that’s the case, the right thing – for me – is to revert and start over. I’ve surely learned something from this and it’ll be better to start with that learning and no code than with code I don’t like.

It’s easily done: Working Copy has a Revert Changes button. And I’m pressing it … now.

Begin Anew

Let’s defer the edge starting issue for now, and deal with waves. Two main aspects. First, when all the asteroids are gone, run another wave, leaving the ship where it is. Second, wave sizes go 4, 6, 8, 10, 11.

I have a feeling I’ll never get there in the game, at least not without hyperspace, or a switch to turn off asteroid-ship collisions.

Are there tests to write? Well, we’ll need to keep the number of ships in the wave in Universe, and increment it in that odd way. So let’s posit a function, Universe:newWaveSize that does that job for us. A side question is whether that will be called at the beginning of the game and my sense of symmetry says that it should be. So a test:

        _:test("Wave size", function()
            local u = Universe()
            _:expect(u:newWaveSize()).is(4)
            _:expect(u:newWaveSize()).is(6)
            _:expect(u:newWaveSize()).is(8)
            _:expect(u:newWaveSize()).is(10)
            _:expect(u:newWaveSize()).is(11)
            _:expect(u:newWaveSize()).is(11)
        end)

First cut at the implementation:

function Universe:newWaveSize()
    self.waveSize = self.waveSize + 2
    if  self.waveSize > 11 then self.waveSize = 11 end
end

(With initializing it to 2 in Universe:init.) I don’t like that, it’s too much of a tricky hack. Let’s see, what would be better?

(Is it driving you crazy that I’m fussing over this tiny detail? It’s driving me crazy too. But this code is fragile. It’s not very critical, but it is fragile. I’m going to remove the init of waveSize from init and make it work here.)

function Universe:newWaveSize()
    self.waveSize = (self.waveSize or 2) + 2
    if  self.waveSize > 11 then self.waveSize = 11 end
    return self.waveSize
end

OK. This is now the only guy who knows there is a waveSize value in U. He takes care of initializing it and updating it. Now lets see about putting it into Universe.

We want Universe to call “newWave” to create the first wave (and any subsequent waves). And while we’re at it, let’s fix this:

function setup()
    U = Universe()
    U:createAsteroids()
end

setup should just create the universe, and universe should create the waves. So I’ll delete that line above and move into the Universe.

We have this:

function Universe:createAsteroids()
    for i = 1,4 do
        local a = Asteroid()
        self.asteroids[a] = a
    end
end

Which I’ll rename newWave and change to call newWaveSize.

My spidey sense is tingling. I have no real reason to change that loop to newWaveSize yet, because there is never a wave bigger than size 4 yet. This is speculative and in principle I should change it when it’s needed.

I’m coding rather poorly today, and I’m aware of it. We have good days and bad ones and today is at best a mediocre one. But I’m sort of having fun so let’s continue.

I’m going ahead and putting in the call to newWaveSize:

function Universe:newWave()
    for i = 1, self:newWaveSize() do
        local a = Asteroid()
        self.asteroids[a] = a
    end
end

Super! That misbehaves in a lovely new way. self.asteroids isn’t initialized yet, because of where I put the create in the init. I put it there in the middle, aware of the general problem, but I didn’t expect it to explode.

What we should do in init is initialize all the constants, then all the empty tables, and then we should create stuff. For now, I’ll just reorder the method … and then refactor:

function Universe:init()
    self.processorRatio = 1.0
    self.score = 0
    self.missileVelocity = vec2(MissileSpeed,0)
    self.button = {}
    self.asteroids = {}
    self.missiles = {}
    self.explosions = {}
    createButtons()
    self:newWave()
    self.ship = Ship()
end

First to make sure we’re still running. And the game runs, but my newWaveSize test fails, returning 2 more than I expect every time. What’s up with that?

Ah. I’ve changed Universe so that it creates a wave right out of the box. So it has already used up 4 and then my test gets 6. I think I’l hammer my private universe back to nil:

        _:test("Wave size", function()
            local u = Universe()
            u.waveSize = nil
            _:expect(u:newWaveSize()).is(4)
            _:expect(u:newWaveSize()).is(6)
            _:expect(u:newWaveSize()).is(8)
            _:expect(u:newWaveSize()).is(10)
            _:expect(u:newWaveSize()).is(11)
            _:expect(u:newWaveSize()).is(11)
        end)

Having this new test, and my other ones, is giving me a bit of comfort, as it always does. So that’s good. Now what should really happen here?

Well, the game should idle, in “attract mode”, with asteroids floating around, until the user puts in his quarters and presses the start button. Then the screen clears and the game starts.

Let’s make this happen, shall we? How shall we signal the start? We could make a new start button, but that’s more than I want to do right now. Let’s do this.

When Universe inits, it sets self.attractMode. It creates a wave of asteroids but nothing else, so they float around looking good. Then, in Main, we’ll check for a screen touch of any kind, in attract mode, and when we get one, we’ll call U:startGame. That should be “easy”.

function Universe:init()
    self.processorRatio = 1.0
    self.score = 0
    self.missileVelocity = vec2(MissileSpeed,0)
    self.button = {}
    self.asteroids = {}
    self.missiles = {}
    self.explosions = {}
    self.attractMode = true
    self:newWave()
end

function Universe:startGame()
    createButtons()
    self.ship = Ship()
    self.asteroids = {}
    self:newWave()
end
function touched(touch)
    if U.attractMode and touch.state == ENDED then U:startGame() end
    if touch.state == ENDED or touch.state == CANCELLED then
        Touches[touch.id] = nil
    else
        Touches[touch.id] = touch
    end
end

That actually works, except (you guessed it) we get six asteroids, not 4 at the beginning. So:

function Universe:startGame()
    createButtons()
    self.ship = Ship()
    self.asteroids = {}
    self.waveSize = nil
    self:newWave()
end

Ha. That worked except for not clearing attractMode:

function Universe:startGame()
    self.attractMode = false
    createButtons()
    self.ship = Ship()
    self.asteroids = {}
    self.waveSize = nil
    self:newWave()
end

OK, now we have a game start thing. We could put a “touch screen to start” message up, and maybe we should do that. I think for now, I’ll put that in Main, but that’s only good for the short term.

function draw()
    U:draw()
    if U.attractMode then
        pushStyle()
        fontSize(50)
        fill(255,255,255, 128)
        text("TOUCH SCREEN TO START", WIDTH/2, HEIGHT/4)
        popStyle()
    end
end

So now it goes like this:

start game

That should be enough to prove that I’m terrible at playing the game with the tablet in keyboard position. Also it’s clear that rotation is too slow. But let’s commit, we have something working: “game start and newWave”.

Turning

Let’s double the turning rate and if it isn’t already a Universe constant, make it so:

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

function Ship:move()
    if U.button.left then self.radians = self.radians + rotationStep end
    if U.button.right then self.radians = self.radians - rotationStep end
    if U.button.fire then if not self.holdFire then self:fireMissile() end end
    if not U.button.fire then self.holdFire = false end
    self:actualShipMove()
end

Let’s move constant to Universe and double it:

function Universe:init()
    self.processorRatio = 1.0
    self.score = 0
    self.rotationStep = math.rad(2) -- degrees
    self.missileVelocity = vec2(MissileSpeed,0)
    self.button = {}
    self.asteroids = {}
    self.missiles = {}
    self.explosions = {}
    self.attractMode = true
    self:newWave()
end


function Ship:move()
    if U.button.left then self.radians = self.radians + U.rotationStep end
    if U.button.right then self.radians = self.radians - U.rotationStep end
    if U.button.fire then if not self.holdFire then self:fireMissile() end end
    if not U.button.fire then self.holdFire = false end
    self:actualShipMove()
end

That’s actually a bit too fast, I think. And doesn’t it need to be adjusted by the processor speed? I think it does. I want knowledge of that ratio to stay inside Universe, so a new function to return adjustedRotationSpeed:

    self.rotationStep = math.rad(1.5) -- degrees

function Universe:adjustedRotationStep()
    return self.processorRatio*self.rotationStep
end


function Ship:move()
    if U.button.left then self.radians = self.radians + U:adjustedRotationStep() end
    if U.button.right then self.radians = self.radians - U:adjustedRotationStep() end
    if U.button.fire then if not self.holdFire then self:fireMissile() end end
    if not U.button.fire then self.holdFire = false end
    self:actualShipMove()
end

That’s a bit better, perhaps a bit slow. I’m definitely not good at controlling this thing in typing position, and not much better when flat. I suspect the controls need to be repositioned or refined.

I don’t remember whether I mentioned that Dave1707 from the Codea forum moved “left” just above “right,” both on the left edge and found that better. We may need to provide a control adjustment mode but I’m sure that’ll be much later.

Anyway turning is better. Commit: “turn parameterized, ratio’d and sped up”

Enough for now, I think …

Summing Up

I was sufficiently inept today to make me want to ditch this article. Even though I like showing things “warts and all”, today I was fumbling around rather more than I’m used to.

Writing the tests helped to get me on the right track, and changing to a different feature helped as well. Kind of a reset for the mind.

Alls well that ends, though, and we have an attract mode screen, and a touch to start and we’re ready for multiple waves. I may have to implement a special “Ron” mode that kills all the current asteroids to ever get to a new wave. Or maybe it just protects me from collisions.

The Universe is slightly better organized, so that’s an improvement.

I’ll break for now and perhaps come back later today. Meanwhile, feel free to point at me and laugh.

I’ll include a copy of CodeaUnit as I use it, in case you want to play with the tests. It’ll be down at the bottom, so be careful what you copy and paste.

See you next time!

Today’s Code


--# Main
-- Asteroids
-- RJ 20200511

Touches = {}

function setup()
    U = Universe()
end

function draw()
    U:draw()
    if U.attractMode then
        pushStyle()
        fontSize(50)
        fill(255,255,255, 128)
        text("TOUCH SCREEN TO START", WIDTH/2, HEIGHT/4)
        popStyle()
    end
end

function touched(touch)
    if U.attractMode and touch.state == ENDED then U:startGame() end
    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)
        
        _:test("Missile fired at rest", function()
            local ship = Ship()
            local missile = Missile(ship)
            _:expect(missile.step).is(U.missileVelocity)
        end)
        
        _:test("Missile fired north", function()
            local ship = Ship()
            ship.radians = math.pi/2
            local missile = Missile(ship)
            local mx = missile.step.x
            local my = missile.step.y
            _:expect(mx).is(0, 0.001)
            _:expect(my).is(U.missileVelocity.x, 0.001)
        end)
        
        _:test("Missile fired from moving ship", function()
            local ship = Ship()
            ship.step = vec2(1,2)
            local missile = Missile(ship)
            local mx = missile.step.x
            local my = missile.step.y
            _:expect(mx).is(U.missileVelocity.x + 1, 0.001)
            _:expect(my).is(U.missileVelocity.y + 2, 0.001)
        end)
        
        _:test("Asteroids increment score", function()
            local a = Asteroid()
            U.score = 0
            scoreAsteroid(a)
            _:expect(U.score).is(20)
        end)
        
        _:test("Wave size", function()
            local u = Universe()
            u.waveSize = nil
            _:expect(u:newWaveSize()).is(4)
            _:expect(u:newWaveSize()).is(6)
            _:expect(u:newWaveSize()).is(8)
            _:expect(u:newWaveSize()).is(10)
            _:expect(u:newWaveSize()).is(11)
            _:expect(u:newWaveSize()).is(11)
        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

Ship = class()

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

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

function Ship:move()
    if U.button.left then self.radians = self.radians + U:adjustedRotationStep() end
    if U.button.right then self.radians = self.radians - U:adjustedRotationStep() end
    if U.button.fire then if not self.holdFire then self:fireMissile() end end
    if not U.button.fire then self.holdFire = false end
    self:actualShipMove()
end

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

function Ship:finallyMove()
    U:moveObject(self)
end

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

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

--# Button
-- Button
-- RJ 20200520

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()
    U.button.left = false
    U.button.right = false
    U.button.go = false
    U.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
                U.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 U.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

Asteroid = class()

function Asteroid:init()
    self.pos = vec2(math.random(WIDTH), math.random(HEIGHT))
    self.shape = Rocks[math.random(1,4)]
    self.scale = 16
    local angle = math.random()*2*math.pi
    self.step = vec2(Vel,0):rotate(angle)
end

function Asteroid:killDist()
    local s = self.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 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
    U.score = U.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 = Asteroid()
    new.pos = asteroid.pos
    new.scale = asteroid.scale
    asteroids[new] = new
    Splat(asteroid.pos)
end

function Asteroid:draw()
    pushMatrix()
    pushStyle()
    translate(self.pos.x, self.pos.y)
    scale(self.scale)
    strokeWidth(1/self.scale)
    for i,l in ipairs(self.shape) do
        line(l.x, l.y, l.z, l.w)
    end
    popStyle()
    popMatrix()
end

function Asteroid:move()
    U:moveObject(self)
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

Missile = class()

function Missile:init(ship)
    function die()
        self:die()
    end
    self.pos = ship.pos
    self.step = U.missileVelocity:rotate(ship.radians) + ship.step
    U.missiles[self] = self
    tween(3, self, {}, tween.easing.linear, die)
end

function Missile:die()
    U.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()

local MissileSpeed = 2.0

function Universe:init()
    self.processorRatio = 1.0
    self.score = 0
    self.rotationStep = math.rad(1.5) -- degrees
    self.missileVelocity = vec2(MissileSpeed,0)
    self.button = {}
    self.asteroids = {}
    self.missiles = {}
    self.explosions = {}
    self.attractMode = true
    self:newWave()
end

function Universe:startGame()
    self.attractMode = false
    createButtons()
    self.ship = Ship()
    self.asteroids = {}
    self.waveSize = nil
    self:newWave()
end

function Universe:draw()
    displayMode(FULLSCREEN_NO_BUTTONS)
    pushStyle()
    background(40, 40, 50)
    self.processorRatio = DeltaTime/0.0083333
    self:drawAsteroids()
    self:drawExplosions()
    checkButtons()
    drawButtons()
    if self.ship then self.ship:draw() end
    if self.ship then self.ship:move() end
    self:drawMissiles()
    drawSplats()
    U:drawScore()
    popStyle()
    U:findCollisions()
end

function Universe:newWave()
    for i = 1, self:newWaveSize() do
        local a = Asteroid()
        self.asteroids[a] = a
    end
end

function Universe:findCollisions()
    for i,a in pairs(self.asteroids) do
        self:checkMissileCollisions(a)
        if self.ship then self:checkShipCollision(a) end
    end
end

function Universe:checkShipCollision(asteroid)
    if self.ship.pos:dist(asteroid.pos) < asteroid:killDist() then
        scoreAsteroid(asteroid)
        splitAsteroid(asteroid, self.asteroids)
        self:killShip()
    end
end

function Universe:checkMissileCollisions(asteroid)
    for k,m in pairs(self.missiles) do
        if m.pos:dist(asteroid.pos) < asteroid:killDist() then
            scoreAsteroid(asteroid)
            splitAsteroid(asteroid, self.asteroids)
            m:die()
        end
    end
end

function Universe:killShip()
    local f = function()
        self.ship = Ship()
    end
    Explosion(U.ship)
    U.ship = nil
    tween(6, self, {}, tween.easing.linear, f)
end

function Universe:moveObject(anObject)
    local pos = anObject.pos + self.processorRatio*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

function Universe:drawExplosions()
    for k,e in pairs(self.explosions) do
        e:draw()
    end
end

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

function Universe:drawAsteroids()
    pushStyle()
    stroke(255)
    fill(0,0,0, 0)
    strokeWidth(2)
    rectMode(CENTER)
    for i,asteroid in pairs(self.asteroids) do
        asteroid:draw()
        asteroid:move()
    end
    popStyle()
    killDeadAsteroids(self.asteroids)
end

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

function Universe:newWaveSize()
    self.waveSize = (self.waveSize or 2) + 2
    if  self.waveSize > 11 then self.waveSize = 11 end
    return self.waveSize
end

function Universe:adjustedRotationStep()
    return self.processorRatio*self.rotationStep
end

--# Explosion
Explosion = class()

function Explosion:init(ship)
    local f = function()
        U.explosions[self] = nil
    end
    self.pos = ship.pos
    self.step = vec2(0,0)
    U.explosions[self] = self
    tween(4, self, {}, tween.easing.linear, f)
end

function Explosion:draw()
    pushStyle()
    pushMatrix()
    translate(self.pos.x, self.pos.y)
    fontSize(30)
    text("BLAMMO", 0, 0)
    popMatrix()
    popStyle()
end

## CodeaUnit ====================================== —

--# CodeaUnit
CodeaUnit = class()

function CodeaUnit:describe(feature, allTests)
    self.tests = 0
    self.ignored = 0
    self.failures = 0
    self._before = function()
    end
    self._after = function()
    end

    print(string.format("Feature: %s", feature))

    allTests()

    local passed = self.tests - self.failures - self.ignored
    local summary = string.format("%d Passed, %d Ignored, %d Failed", passed, self.ignored, self.failures)

    print(summary)
end

function CodeaUnit:before(setup)
    self._before = setup
end

function CodeaUnit:after(teardown)
    self._after = teardown
end

function CodeaUnit:ignore(description, scenario)
    self.description = tostring(description or "")
    self.tests = self.tests + 1
    self.ignored = self.ignored + 1
    if CodeaUnit.detailed then
        print(string.format("%d: %s -- Ignored", self.tests, self.description))
    end
end

function CodeaUnit:test(description, scenario)
    self.description = tostring(description or "")
    self.tests = self.tests + 1
    self._before()
    local status, err = pcall(scenario)
    if err then
        self.failures = self.failures + 1
        print(string.format("%d: %s -- %s", self.tests, self.description, err))
    end
    self._after()
end

function CodeaUnit:expect(conditional)
    local message = string.format("%d: %s", (self.tests or 1), self.description)

    local passed = function()
        if CodeaUnit.detailed then
            print(string.format("%s -- OK", message))
        end
    end

    local failed = function()
        self.failures = self.failures + 1
        local actual = tostring(conditional)
        local expected = tostring(self.expected)
        print(string.format("%s -- Actual: %s, Expected: %s", message, actual, expected))
    end

    local notify = function(result)
        if result then
            passed()
        else
            failed()
        end
    end

    local is = function(expected, epsilon)
        self.expected = expected
        if epsilon then
            notify(expected - epsilon <= conditional and conditional <= expected + epsilon)
        else
            notify(conditional == expected)
        end
    end

    local isnt = function(expected)
        self.expected = expected
        notify(conditional ~= expected)
    end

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

    local hasnt = function(expected)
        self.expected = expected
        local missing = true
        for i,v in pairs(conditional) do
            if v == expected then
                missing = false
            end
        end
        notify(missing)
    end
    
    local throws = function(expected)
        self.expected = expected
        local status, error = pcall(conditional)
        if not error then
            conditional = "nothing thrown"
            notify(false)
        else
            notify(string.find(error, expected, 1, true))
        end
    end

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

CodeaUnit.execute = function()
    for i,v in pairs(listProjectTabs()) do
        local source = readProjectTab(v)
        for match in string.gmatch(source, "function%s-(test.-%(%))") do
            print("loading", match)
            loadstring(match)()
        end
    end
end

CodeaUnit.detailed = true



_ = CodeaUnit()

parameter.action("CodeaUnit Runner", function()
    CodeaUnit.execute()
end)
--# Main
function setup()
    img = readImage(asset.documents.unitpic)
    sprite(img,500,600)
    saveImage(asset.."Icon",img)
end

function draw()
    sprite(img, 500,600)
end