A report on last night, and a ‘major’ refactoring. P.S. There is no such thing as a major refactoring.

Report

Again last night, I did a bit of programming without concurrent writing. Frankly, it’s much more relaxing. I accomplished three things:

  1. Adjusted motion speed to draw-cycle time;
  2. Implemented simple scoring;
  3. Adjusted asteroid hit radii based on size.

We’ll briefly review how those were done: they’re all rather simple.

Adjusted motion speed

It turns out that Codea’s draw cycle time isn’t always one sixtieth of a second as I thought. On fast enough iPads, like this one, it runs at 1/120 if it can. If drawing lags behind, not finishing in time for the next cycle, it steps down to 1/60. It can even step down to 1/30 if it needs to.

I discovered this when I thought I was observing a variance in speed of things on the screen. I did some tests and it became clear that my iPad was running at 1/120. An inquiry to the Codea forum confirmed the information above.

The speed at which things move in the version we last looked at is based on Vel, which I set to 1.5 by inspection. That means that a moving object moves a distance of 1.5 in screen coordinates every draw cycle. The screen is 1366 wide by 1024 high. Pixels are “square”, so that a 45 degree angle looks like 45 degrees.

So an object going straight up would need 1024/1.5 cycles, or 683 cycles, to move from bottom to top. Dividing by 120 gives us about 5.7 seconds to cross the screen vertically. Eyeballing the screen tells me that’s about right.

But if we run this code on an iPad that cycles at 1/60, everything slows down by a factor of two. We’d like to do something to speed it back up, and what we can do is move twice as far in each cycle.

Codea maintains a global DeltaTime, which is the actual time between the previous draw cycle and the current one. The 1/120 cycle time is about 0.0083333 seconds. Suppose we set:

Ratio = DeltaTime/0.0083333

Then if we’re on a slow iPad, Ratio will be about 0.01666/0.0083333, or about 2. If we multiply our increments to position by Ratio, we should get a constant speed for things.

And that’s what I did. At the top of the main draw, I calculate Ratio:

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

Only asteroids and missiles move so far. Splats do not move, they just expand. Splats use a tween, which is already enumerated in seconds, so they should be OK. So in the moving code, we have these changes:

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 Missile:move()
    self.pos = self.pos + Ratio*self.vel
    self.pos = vec2(keepInBounds(self.pos.x, WIDTH), keepInBounds(self.pos.y, HEIGHT))
end

So that’s speed correction, for now. The fact that we have to remember to put it into everything that moves is troubling. Surely I’ll forget something, so it would be good to find a way to centralize that facility.

I also note that the main draw is getting pretty messy, and it irritates me that moving an asteroid and moving a missile are done differently. I’d like to clean that up.

Simple Scoring and Hit Radius

Asteroids scores 20 for breaking a large asteroid, 50 for a medium, and 100 for a small. I did a quick and dirty scoring feature, like this:

In setup, I set Score to zero. Then in findCollisions, I call score(asteroid):

function findCollisions()
    local KillDist = 50
    for i,a in pairs(Asteroids) do
        for k,m in pairs(Missiles) do
            if m.pos:dist(a.pos) < killDist(a) then
                scoreAsteroid(a)
                splitAsteroid(a)
                m:die()
            end
        end
    end
end

Then in the Asteroid tab:

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

We compute score based on asteroid size and add it into Score. I thought at first of looking the score up in a table but decided to go with the if for now.

Finally, I display the score near the end of draw, calling drawScore, which looks like this:

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

Convert score to string, slam on leading zeros, take five digits, display. Codea does have a format function but this seemed more in the spirit of 1979. Here’s a pic:

score

Notice the circles in each of the asteroids? I popped those in to see whether just checking the distance between missile and asteroid center would suffice for collisions. What we see in that picture is that the implementor of the original Asteroids was very careful to shape them so that a simple distance check would work well.

I did that in three new commits, between 9:30 and 10:00PM last night.

I’ll spare you the code for now and make sure there’s a copy at the end.

And Now What?

Let’s think about what features we need, or may need, and what code improvements are indicated.

Features include ship movement, ship destruction when hit by asteroids, multiple ships per “game”, perhaps a two-player mode, limiting shots to four at a time, hyperspace, and the two saucers that come out and try to kill you. And maybe some sounds. I suppose one could even have an idling mode like the real game. And probably there are more features I’ve forgotten. Ha! Game restarting after all your ships are gone, for one almost forgotten example.

Code improvements include, hmm …

Normalize Style
I prefer the object-oriented style of Splat and Missile, and would like to convert Asteroid and Ship to be compatible with that.
Drawing, Moving, Colliding
The current approach to drawing and moving and finding collisions is tangled at best. The “right thing” in a video game is to draw everything, move everything, check for collisions and other events, and then loop back to draw everything. We’re working OK now but it’s confusing and likely to lead to mistakes, especially as we add in saucers and such.
Object Collections
There’s no uniform handling of the various objects, and there are random global variables around, holding groups of things, or just providing shared access like Button does. It would be nice to centralize management of all the “stuff” somehow. That way we’d know where to look for things when we need to change them.
Game Creation
Everything is set up in, well, setup, but there are tables that are initialized only in their declaration, spread among tabs, and other oddities that I’ve surely forgotten but that will turn up when we start looking around
Magic Numbers
There are magic numbers all around, speeds and ratios and scores and angles. These ought to be brought together in as few places as possible, and perhaps even given better names. If there are required relationships among them, those should be made explicit.

And surely there’s more.

And this, mind you, is the mess we’ve created in less than 500 lines of code, counting blank lines. We may have another 500 to go, at a random guess, and the going won’t get smoother.

So I’d like to spend a bit of time today smoothing out some of these bumps.

Where shall we start?

The Universe

I think I’d like to start at the top, with the universe. We have a number of nearly global structures, mostly tables and some constants. Things are fairly well isolated into tabs now, but there are still the collections at the top of most of the tabs, and some code, like the findCollisions that knows where to look for things.

Now there are some fancy patterns we could use to pass collections around when needed, but the Codea draw cycle limits us in that nothing is passed into that function, which means that anything we did pass around would have to be discovered there and passed in. That might still be desirable: we’ll see. Right now the structure isn’t clear enough in my mind for me to imagine a perfect better one.

I propose a new class, Universe, with one instance, U, that will contain all the collections, magic constants, and global behaviors that we need. We’ll move things to it slowly, to get a feeling for whether it’s working out.

I think the first thing to move into it will be the Asteroids collection. I’m thinking it’s big enough to be interesting and hoping that it’s small enough not to be a problem. If it is, we’ll try something else.

So here goes. With the Codea-provided touch function and the comments removed, and my header comment added, it looks like this:

-- Universe
-- RJ 20200523

Universe = class()

function Universe:init()
end

function Universe:draw()
end

So far so good. My tentative plan is that Universe has member variables that are the various global tables and values we use throughout the program. We might bury values another level down in something like U.constants.speed or the like. We’ll see.

My plan is that Main will create the universe and the universe will create everything else. We’ll do this incrementally of course.

In Main:

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

I just created a Universe and told it to create the asteroids. If I run now, nothing good happens, because there will be no asteroids. Now to fix that:

function Universe:createAsteroids()
    createAsteroids()
end

That works, trivially, because there’s already a function over in Asteroids to do that. I want the Asteroids table inside the Universe. Right now, it’s a local in Asteroid tab, as is DeadAsteroids, the table that stores moribund asteroids until it is safe to edit the table.


-- Asteroid
-- RJ 20200520

Asteroids = {}
local DeadAsteroids = {}
local Vel = 1.5

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

My plan is to remove the global Asteroids here, create a member variable named asteroids in Universe, and pass that collection to the creator and whoever else needs it. This will break stuff and Codea doesn’t have much refactoring support, by which I mean none.

Universe = class()

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

function Universe:draw()
end

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

And in Asteroid tab:

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

Now a quick search for who else needs that global. Inside the tab we find a few references. I think I’ll just take them one by one:

function drawAsteroids()
    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()
end

This loops over all the asteroids. if we pass it the asteroids collection, it’ll do fine, though it will need to pass it on to the kill function, I reckon. (I’m starting to wish this Asteroid thing was an object but no matter.)

Let’s make Main call U:draw() and begin to move things like moving and drawing the asteroids into U.


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()
    findCollisions()
end

In Universe:

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

And in Asteroid:

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

I took a flyer and passed the collection into the kill operation. Better look at it and clean it up:

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

There’s one more reference to the old global left in the Asteroid tab (assuming I’ve spotted them all):

function splitAsteroid(asteroid)
    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

The splitAsteroid function is called from … Main tab:

function findCollisions()
    local KillDist = 50
    for i,a in pairs(Asteroids) do
        for k,m in pairs(Missiles) do
            if m.pos:dist(a.pos) < killDist(a) then
                scoreAsteroid(a)
                splitAsteroid(a)
                m:die()
            end
        end
    end
end

Let’s move that to U and have Main just call it. Then we have legit access to the asteroids collection.

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, asteroids)
                m:die()
            end
        end
    end
end

Note that I’m using the asteroids collection in U twice there, the second time passing it to the splitter.

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

At this point I want to run the program and see what explodes. I’m hoping it’s just asteroids.

And it runs. By Jove, I think we’ve done it. One Codea search to see if there is a capital-A “Asteroids” left but I don’t expect to find one … and there is not.

We have successfully created the Universe and have moved some key functions into it, creation of the asteroids, plus their drawing, plus collision detection. We’ve hidden the Asteroids global, although we’ve replaced it with another global, U, a much more capable object and a rather nice place for things to live.

That’s enough for today. I’ll sum up, and paste another copy of the code down below the starting version.

In due time, I plan to keep the current version live in a standard place, as suggested by Dave1707, but for today, we’ll keep it here.

Summing Up

We’ve observed some systematic structural concerns in our code. It’s the sort of thing that might have tempted us to do some kind of major rewriting of parts of it. We resisted that bravely, and instead created a very simple object with only four methods, init, draw, createAsteroids, and findCollisions.

Three of those methods were trivial and the fourth was just moved, with a light edit, from another location. It went smoothly and we were never deeply confused, and things work.

I’m a bit less happy than I’d be if I had lots of micro-tests, but I still see no good way to test the things I’m worried about. Maybe we’ll talk about that next time. In any case the game still plays as before.

So we’ve done an hour’s work or less, and taken a decent first step toward an improved structure. I’ve noticed that we’d have done well to have imported createAsteroids as we did the find, but we can do that another time. And there’s a lesson there, which is that there’s a perfectly good first step to consolidating things in place, and another step to take if and when we want to.

To me, there is a big lesson here, which is that we can always (OK, nearly always) improve the code in small safe steps, without rewriting or stalling the project while we clean things up. And that’s a much better way to go, because the Powers That Be really don’t like it when projects stall.

I like to say that there is no such thing as a “major refactoring”, and this little example shows us why I say that. There are always small safe steps to be taken. We just have to find them.

See you next time!


The Starting Code

As of starting new work Saturday:

--# Main
-- Asteroids
-- RJ 20200511

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

function setup()
    print("Hello Asteroids!")
    print(WIDTH, HEIGHT)
    Score = 0
    --displayMode(FULLSCREEN_NO_BUTTONS)
    createButtons()
    createAsteroids()
    createShip()
end

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

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

function findCollisions()
    local KillDist = 50
    for i,a in pairs(Asteroids) do
        for k,m in pairs(Missiles) do
            if m.pos:dist(a.pos) < killDist(a) then
                scoreAsteroid(a)
                splitAsteroid(a)
                m:die()
            end
        end
    end
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(keepInBounds(100,1000)).is(100)
            _:expect(keepInBounds(1000,1000)).is(0)
            _:expect(keepInBounds(1001,1000)).is(1)
            _:expect(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 = {}

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

function drawShip()
    local sx = 10
    local sy = 6
    pushStyle()
    pushMatrix()
    translate(Ship.pos.x, Ship.pos.y)
    rotate(Ship.ang)
    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.ang = Ship.ang + 1 end
    if Button.right then Ship.ang = Ship.ang - 1 end
    if Button.fire then if not Ship.holdFire then fireMissile() end end
    if not Button.fire then Ship.holdFire = false 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

Asteroids = {}
local DeadAsteroids = {}
local Vel = 1.5

function createAsteroids()
    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.angle = math.random()*2*math.pi
    a.shape = Rocks[math.random(1,4)]
    a.scale = 16
    return a
end

function drawAsteroids()
    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()
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()
    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)
    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)
    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


--# 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.vel = vec2(MissileVelocity,0):rotate(math.rad(ship.ang))
    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()
    self.pos = self.pos + Ratio*self.vel
    self.pos = vec2(keepInBounds(self.pos.x, WIDTH), keepInBounds(self.pos.y, HEIGHT))
end

The Ending Code

Here’s what we finished with, commit: Created Universe:

--# 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(keepInBounds(100,1000)).is(100)
            _:expect(keepInBounds(1000,1000)).is(0)
            _:expect(keepInBounds(1001,1000)).is(1)
            _:expect(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 = {}

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

function drawShip()
    local sx = 10
    local sy = 6
    pushStyle()
    pushMatrix()
    translate(Ship.pos.x, Ship.pos.y)
    rotate(Ship.ang)
    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.ang = Ship.ang + 1 end
    if Button.right then Ship.ang = Ship.ang - 1 end
    if Button.fire then if not Ship.holdFire then fireMissile() end end
    if not Button.fire then Ship.holdFire = false 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.angle = math.random()*2*math.pi
    a.shape = Rocks[math.random(1,4)]
    a.scale = 16
    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)
    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


--# 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.vel = vec2(MissileVelocity,0):rotate(math.rad(ship.ang))
    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()
    self.pos = self.pos + Ratio*self.vel
    self.pos = vec2(keepInBounds(self.pos.x, WIDTH), keepInBounds(self.pos.y, HEIGHT))
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