Having converted the 6502 asteroid shapes to Codea style, let’s plug them in. This design is starting to bug me, and we’ll think about that too.

Plugging in the new asteroids shapes is as simple as creating a new tab in Codea and pasting in the code generated in the conversion process. Right now, the shapes are defined separately, RR1, RR2, RR3, and RR4. RR stands for RROCK, by the way.

But this code is bugging me

As I tried to bring my uncut hair under some vestige of control this morning, I was thinking about the morning’s work. (It’s about 09:20, by the way, in case we want to consider how long things took this morning.) But I digress. I was thinking, first of all, that I’ll surely want the rock shapes in a table, so that I can easily select one randomly. Right now they are in those four globals, which isn’t very harmful, but it got me thinking about the overall design we have here.

Here is a list of the functions defined in our app so far:

  • setup
  • createAsteroids
  • createAsteroid
  • createButtons
  • createShip
  • draw
  • checkButtons
  • drawButtons
  • drawNewAsteroid
  • drawShip
  • moveShip
  • drawAsteroids
  • drawAsteroid
  • moveAsteroid
  • keepInBounds
  • touch

These functions are all in one tab, Main. They are all global functions, visible to each other and to anything else we ever do.

This is the kind of design you get when your thinking is primarily procedural, and when you just sort of begin at one end and program until you’re done or time runs out. Everything all at the top level, and, if you’re a bit careful with the naming, the names reflect something about the design – the design in your head, that’s not very well reflected in the code.

There are six functions with “Asteroid” in their name. Three with “Ship”. Three with “Buttons”. Those names say that there are those kinds of things in our mind, but they’re reflected only very weakly in the code.

If you’re anywhere near typical as a programmer, you’ve seen code like this in real life: some huge file, full of code that does all kinds of random stuff. If you’re like me, you’ve written some code like that. Most of us learn programming in a way that invites us to write code without much structure, because we don’t learn structuring approaches early on. And when we encounter examples, they look like this. And when we encounter working code that we have to maintain, it looks like this.

Further, the more code looks like this, the more defects we insert, just because of the difficulty of grasping it all at once. This kind of code literally bugs us.

This is not the way.1

The Way

We can sometimes find things to do that give us a better sense of organization. It would be easy to put all the Asteroids code in a tab called Asteroids, the Buttons in a Buttons tab, and so on. In Codea, this is the same as separating things out into separate files.

Even here there is an objection from some of us.

With everything in one file, if I want to know what some function or variable is, I just search and there it is. No problem. If we start putting things in separate files, then I hardly know where to look and things are harder to find even if I guess right.

I’m not going to handle that objection, I’m going to ignore it and write code as I think it should be written, explaining why as I go. If you like what you see, maybe give it a try. If you don’t, please continue to program in the best way you know. That’ll be fine with me.

So today’s plan is first to plug in our new rock shapes, and then to begin to push this code into a shape that isn’t quite so monolithic as it is right now.

The Rocks

We have our rock shapes, RR1 and so on. We have a table of asteroids, and each asteroid is itself a table, created like this:

function createAsteroid()
   local a = {}
   a.pos = vec2(math.random(WIDTH), math.random(HEIGHT))
   a.angle = math.random()*2*math.pi
   return a
end

For now, let’s just add in a random shape and then edit our drawAsteroid function to use that shape. I’ll start with a table of rocks:

local Rocks = {RR1,RR2,RR3,RR4}

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)]
   return a
end

Now each asteroid table has a new element, shape, which is one of the RROCK tables. At this point the program runs the same as before: the shape isn’t being used.

Here’s the draw as it stands now:

function drawAsteroid(asteroid)
   rect(asteroid.pos.x, asteroid.pos.y, 120)
end

If we replace this with our drawing code from the conversion, we should be pretty close to good.

First I just plug in the drawTable from the conversion program:

function drawAsteroid(asteroid)
    drawTable(asteroid.pos.x, asteroid.pos.y, asteroid.shape)
end

function drawTable(x,y, tab)
    pushMatrix()
    pushStyle()
    translate(x,y)
    scale(10)
    strokeWidth(1/10)
    for i,l in ipairs(tab) do
        line(l.x, l.y, l.z, l.w)
    end
    popStyle()
    popMatrix()
end

Why? Because I want to change as little as possible to get back to working as soon as possible. And with one minor glitch, that works as expected. Here’s a screen shot:

new shapes

The glitch was something I worried about but tried anyway. In Main, I just wrote that statement:

  local Rocks = {RR1,RR2,RR3,RR4}

This didn’t work, the table Rocks was empty. That’s because Codea compiles its tabs in left to right order. As one does, I suppose. Since I have Main first, the table was {nil,nil,nil,nil} and that’s the same as {}.

I moved the Rocks definition into the Shapes tab and left it global and things run fine. It’s another odd design glitch that is part of what’s bugging me.

Anyway, commit: “New Shapes for Rocks”.

Now I’ll fold that drawTable into drawAsteroid, remove the code that draws the one in the middle, and then we’ll look for larger prey.

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

So that’s reasonable. It kind of bugs me that Codea isn’t consistent with the use of free-standing x and y coordinates and vector coordinates. Arguably our asteroid table should contain x and y rather than pos. Small issue and perhaps we’ll return to it.

For now …

How can we clean this baby up?

We could begin by doing nothing more than moving separate bits that belong together into separate tabs. I’ve never done that with Codea before: I generally create classes earlier. This Asteroids exercise has been intended to avoid that for a while, letting the code get more procedural, more in-line than I’d usually do.

My thinking is that folks new to Codea often start out with this kind of solution that just grows and gets more and more entangled. I know this because I see on the forum what they’re doing. Often, my more experienced eye (the left one) sees that part of their trouble is just in the organization of the code, not in a failure to know how to write code.

So here, the idea is to get into somewhat bad shape and then move to get out.

Let’s try the tab trick. I’ll move each type of code to its own tab. Then we’ll see what we see. I’ll do the ship first, for no particular reason.

First, though, commit. “Remove Center Rock, improve draw”

Ship

 -- Ship
-- RJ 20200520

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
end

To make this work, I had to make two variables global, Ship and Button. Ship is referred to above, as is Button. I think I can move the Ship variable into this file and leave it local. Let’s see.

Yes, that works. Buttons are tricky, because the Button variable is a shared structure between the buttons and the ship. The buttons set Button.left=true and so on, and the ship checks them.

We could finesse that in a number of ways. One idea is that the Buttons don’t care what kind of a table they set their values into, so they could slam left and right directly into the Ship if we wanted.

But right now, we’re just here to get things in separate tabs. We’ll worry about connections later.

I think I’ll move the buttons next:

-- 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

I had to make Touches global for this to work. Main currently looks like this:

-- Asteroids
-- RJ 20200511

local Asteroids = {}
local Vel = 1.5
Touches = {}

function setup()
    print("Hello Asteroids!")
    displayMode(FULLSCREEN_NO_BUTTONS)
    createButtons()
    createAsteroids()
    createShip()
end

function createAsteroids()
    for i = 1,10 do
        table.insert(Asteroids, createAsteroid())
    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)]
    return a
end

function draw()
    checkButtons()
    displayMode(FULLSCREEN_NO_BUTTONS)
    pushStyle()
    background(40, 40, 50)
    drawButtons()
    drawShip()
    moveShip()
    drawAsteroids()
    popStyle()
end

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

function drawAsteroid(asteroid)
    pushMatrix()
    pushStyle()
    translate(asteroid.pos.x, asteroid.pos.y)
    scale(10)
    strokeWidth(1/10)
    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 = 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

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

Clearly the next thing is to move the asteroid code to an Asteroid tab:

-- Asteroid
-- RJ 20200520

local Asteroids = {}
local Vel = 1.5

function createAsteroids()
    for i = 1,10 do
        table.insert(Asteroids, createAsteroid())
    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)]
    return a
end

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

function drawAsteroid(asteroid)
    pushMatrix()
    pushStyle()
    translate(asteroid.pos.x, asteroid.pos.y)
    scale(10)
    strokeWidth(1/10)
    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 = 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 moved Asteroids and Vel into this tab, both local. We now have two global variables, Button, and Touches. Main now looks like this:

-- Asteroids
-- RJ 20200511

Touches = {}

function setup()
    print("Hello Asteroids!")
    displayMode(FULLSCREEN_NO_BUTTONS)
    createButtons()
    createAsteroids()
    createShip()
end

function draw()
    checkButtons()
    displayMode(FULLSCREEN_NO_BUTTONS)
    pushStyle()
    background(40, 40, 50)
    drawButtons()
    drawShip()
    moveShip()
    drawAsteroids()
    popStyle()
end

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

That’s nearly good. I probably should have been committing on each tab’s completion, but anyway let’s do it now: Commit: all separate tabs.

Summing up

We have lovely new shapes for our asteroids, and we have the code all nicely broken out by the kind of thing being addressed, Asteroids, Buttons, Ships, Tests, and Shapes.

I think this is enough for today. The next changes that I presently have in mind will require a fresh mind for me and for you. I’ll include all the code here and call it a day.

--# Main
-- Asteroids
-- RJ 20200511

Touches = {}

function setup()
    print("Hello Asteroids!")
    displayMode(FULLSCREEN_NO_BUTTONS)
    createButtons()
    createAsteroids()
    createShip()
end

function draw()
    checkButtons()
    displayMode(FULLSCREEN_NO_BUTTONS)
    pushStyle()
    background(40, 40, 50)
    drawButtons()
    drawShip()
    moveShip()
    drawAsteroids()
    popStyle()
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
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 Asteroids = {}
local Vel = 1.5

function createAsteroids()
    for i = 1,10 do
        table.insert(Asteroids, createAsteroid())
    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)]
    return a
end

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

function drawAsteroid(asteroid)
    pushMatrix()
    pushStyle()
    translate(asteroid.pos.x, asteroid.pos.y)
    scale(10)
    strokeWidth(1/10)
    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 = 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

  1. Mando, private communication.