Dave1707 offers a much better idea for the conversion of Asteroids graphics into Codea. Let’s explore it.

Dave1707, over on the Codea forum, suggested that a better format for the Codea-side asteroid shapes would be vec4, because each instance could contain the starting and ending 2-d points for a segment, and the line drawing would be easier and faster. He’s definitely right.

There are some lessons to learn here. First, I think, is that another set of eyes, and more importantly another brain, looking at the problem and solution, invariably helps. My favorite way to get that effect is with pair programming, and in my experience it is lots more fun, and more productive as well. But even the feedback one gets from an occasional reader or informal code review can be well worth while.

Second, I really think I “should” have thought of that. When setting up a data structure, one of my prime considerations is the convenience of the primary code that uses the structure. My drawing code does that neat trick with saving the previous location and adding it into the next location, because the input is still in the form of incremental x and y. Since we’re dealing with constants throughout, that arithmetic could have been done at the time we created the structure, making the draw code shorter, simpler, and faster.

I’m not going to beat myself up over that “should”, but I am going to think about what made me forget to push as far as I could have into getting the structure just right. If I were looking for an excuse, I’d say that I was trying to be true to the way Asteroids did it but that would be a lie. I fully intend to use Codea’s powers to the limit, to make something that looks and acts like Asteroids, but I’m interested in what that program would look like today, not what it looked like in 1979.

More likely, I had my head in the framework of the Asteroids inch-by-inch way, and didn’t come back and think enough about the Codea way. Plus, I was tired. I was juggling a fair amount of complexity in my head and in my code, so when it worked, I stopped.

That’s a good thing to do. It’s also a good thing to take a look when fresh and see what one can do that would be better. But I can tell you that until Dave offered his idea, I had absolutely no intention of thinking more about this conversion problem. It was a yak I needed to shave, it was shaved, and I was going to move on. Now, with a better way an hour away, I’m going with the better way.

And that offers us a dilemma:

Redo or Reuse?

Dave’s note on the forum includes the asteroids converted to vec4. I could copy/paste them in and move on. For no good reason, I don’t want to do that. Instead, I want to change my conversion code to produce vec4. I guess when I finish this series, I want to have the entire solution written down in these articles and in code on my iPad. I’d like to just adopt Dave’s actual solution, but I feel more like I have to learn from it, not just use it.

So that’s what I’m gonna do. My house, my rules, at least while the wife and cat are napping.

Dave did another thing in his example that needs to be duplicated. Recall that my asteroid drawing starts from (0,0), draws one line invisibly, then continues from there drawing in white. That’s definitely left over from how the original Asteroids worked, since it must start from where it is, and to draw somewhere else, it has to draw an invisible line to that place. No reason for us to do that and it wastes time and space.

So I’m going to convert my conversion to produce a table ofvec4, starting from the end of the first visible line.

Convert? That’s an interesting notion. Should I adjust the conversion program I wrote to do vec4? Or should I do it from scratch, not because it would be more fun but because converting might be awkward.

To decide that, I need to look at the existing converter.

The Existing Converter

Here are the relevant bits of the converter:

function convertTable(tab)
    local result = {}
    local prev = vec2(0,0)
    for i, t in ipairs(tab) do
        local next = cumulateScaled(prev, t)
        table.insert(result, next)
        prev = next
    end
    return result
end

function cumulateScaled(prev, data)
    return cumulate(prev, scaled(data))
end

function cumulate(prev, data)
    return data + prev
end

function scaled(input)
    local mul = (input.s~=3) and 1 or 2
    return vec2(input.x, input.y)*mul
end

And there are tests for this version as well:


function testCodeaUnitFunctionality()
    CodeaUnit.detailed = true

    _:describe("CodeaUnit Test Suite", function()

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

        _:after(function()
            -- Some teardown
        end)

        _:test("cumulate unscaled", function()
            local prev = {x=2,y=3}
            local data = {s=1, x=2, y=4}
            local next = cumulate(prev,data)
            _:expect(next.x).is(4)
            _:expect(next.y).is(7)
        end)
        
        _:test("scale 2 returns unchanged", function()
            local input = {s=2,x=1,y=2}
            local new = scaled(input)
            _:expect(new.x).is(1)
            _:expect(new.y).is(2)
        end)
              
        _:test("scale 3 returns double", function()
            local input = {s=3,x=1,y=2}
            local new = scaled(input)
            _:expect(new.x).is(2)
            _:expect(new.y).is(4)
        end)
        
        _:test("unscaled cumulated", function()
            local prev = vec2(2,3)
            local data = {s=1, x=2, y=4}
            local next = cumulateScaled(prev, data)
            _:expect(next.x).is(4)
            _:expect(next.y).is(7)
        end)
              
        _:test("scaled cumulated", function()
            local prev = vec2(2,3)
            local data = {s=3, x=2, y=4}
            local next = cumulateScaled(prev, data)
            _:expect(next.x).is(6)
            _:expect(next.y).is(11)
        end)
        
        _:test("do a table", function()
            local intab = {
                {s=2, x=1, y=2},
                {s=3, x=3, y=4},
            }
            local outab = convertTable(intab)
            _:expect(#outab).is(2)
            local last = outab[2]
            _:expect(last.x).is(7)
            _:expect(last.y).is(10)
        end)
        
        _:test("square", function()
            local square = {
                {s=3, x =  1, y =  1}, -- /
                {s=3, x =  0, y = -1}, -- |
                {s=3, x = -1, y =  0}, -- _
                {s=3, x =  0, y =  1}, -- |
                {s=3, x =  1, y =  0}  -- _
            }
            local big = convertTable(square)
            _:expect(big[1]).is(vec2(2,2))
            _:expect(big[2]).is(vec2(2,0))
            _:expect(big[3]).is(vec2(0,0))
            _:expect(big[4]).is(vec2(0,2))
            _:expect(big[5]).is(vec2(2,2))
        end)
    end)
end

Looking at this and at the prospect of fixing it up makes me want to just grab Dave’s tables and run. But I do want a complete ability to build, and I’m now aware that I will need to convert at least one more object, the saucers. So here’s my plan. I’m going to start over, inside this same project, removing the tests and operational code, and editing the main so that it won’t get in the way until I’m ready to draw.

In fairness, or as a point of interest, I’ll time how long it takes. If I had a chess clock, I’d split out the time I spend writing, but since there’s thinking going on, at least some of the writing time counts as programming time.

09:36AM

09:39

I’ve deleted all the operational code in the test tab, all the tests except for a hookup, and commented our or deleted everything in Main that relies on things working. Hookup is green.

Now what. My input looks like a table of scale, x, and y values. I want a table of vec4 values. I think I’ll write a full test of that first, at least to express what I need. Here’s the table:

            local tab = {
                {s=3, x=0, y=2},
                {s=3, x=2, y=2},
                {s=3, x=0, y=1}
            }

I picked s = 3 because that’s the value of s that doesn’t scale. So the output should be …

            local expected = {
                vec4(0,2, 2,4),
                vec4(2,4, 2,5)
            }

At least I think that’s what it should be. Let’s see what the code thinks. Here’s my test:

        _:test("small table", function()
            local tab = {
                {s=3, x=0, y=2},
                {s=3, x=2, y=2},
                {s=3, x=0, y=1}
            }
            local expected = {
                vec4(0,2, 2,4),
                vec4(2,4, 2,5)
            }
            local check = convertTable(tab)
            _:expect(#check).is(#expected)
            for i,e in ipairs(expected) do
                local c = check[i]
                _:expect(c).is(e)
            end
        end)

I’m checking the size of the output array against the expected and then each individual item. Let’s build up a series of less-wrong versions of convertTable functions:

09:52

function convertTable()
    
end

Well that’s not very good, let’s return an empty table. That fails all the tests (CodeaUnit doesn’t stop on the first error.)

Return the input:

function convertTable(tab)
    return {tab}
end

That’s odd, it said “Actual: 1, Expected: 2.

Oh. I changed my mind mid-stream. I was going to return a literal table, then thought of returning tab. I meant to say:

function convertTable(tab)
    return tab
end

There we go, actual 3 expected 2. Maybe we should do some work now.

I basically just typed this in:

function convertTable(tab)
    local out = {}
    local sum = vec2(0,0)
    for i, e in ipairs(tab) do
        local v = vec2(e.x, e.y)+sum
        if i > 0 then
            local v4 =  vec4(sum.x, sum.y, v.x, v.y)
            sum = v
            table.insert(out,v4)
        end
    end
    return out
end

It fails, generating 3 items instead of two. The bug is the i > 0. I meant to say i > 1 because Lua arrays start at 1. I know that, I thought it, and somehow typed zero anyway. Let’s try that fix:

That gets the right number of elements but the output starts at 0,0, not 0,2 as it should.

I should scratch this and start over more simply. But I can’t resist one more attempt to get it right. Whew!

function convertTable(tab)
    local out = {}
    local sum = vec2(0,0)
    for i, e in ipairs(tab) do
        local v = vec2(e.x, e.y)+sum
        if i == 1 then
            sum = v
        else
            local v4 =  vec4(sum.x, sum.y, v.x, v.y)
            sum = v
            table.insert(out,v4)
        end
    end
    return out
end

I needed to update sum with the first value. Now this code can be optimized: the sum = v can be moved out and the if collapsed again. I’ll do that in two steps:

function convertTable(tab)
    local out = {}
    local sum = vec2(0,0)
    for i, e in ipairs(tab) do
        local v = vec2(e.x, e.y)+sum
        if i == 1 then
        else
            local v4 =  vec4(sum.x, sum.y, v.x, v.y)
            table.insert(out,v4)
        end
        sum= v
    end
    return out
end

Test still green. And then …

function convertTable(tab)
    local out = {}
    local sum = vec2(0,0)
    for i, e in ipairs(tab) do
        local v = vec2(e.x, e.y)+sum
        if i > 1 then
            local v4 =  vec4(sum.x, sum.y, v.x, v.y)
            table.insert(out,v4)
        end
        sum= v
    end
    return out
end

10:13

So. I suspect this is actually right and it’s 10:13, so that has taken so far, 37 minutes. I’ll accept that. Now for a wild leap: I’m going to convert one of the real tables and try to draw it.

ha ha close

Well that was close but of course I forgot to do the scaling. I really should write a test for that but I’m out of control now and I’m just going to hammer it in:

function convertTable(tab)
    local out = {}
    local sum = vec2(0,0)
    for i, e in ipairs(tab) do
        local mul = e.s~=3 and 1 or 2
        local v = vec2(e.x, e.y)*mul + sum
--        local v = vec2(e.x, e.y)+sum
        if i > 1 then
            local v4 =  vec4(sum.x, sum.y, v.x, v.y)
            table.insert(out,v4)
        end
        sum= v
    end
    return out
end

That draws correctly:

one correct

I got away with that. A wise man would have a test for it. I am not a wise man, not today.

10:24

Not an hour yet. Let’s draw all four:

all four

That’s good. Now the formatting: It starts like this in its “make it run” form:

function printTable(name, tab)
    local o = ""
    --[[
    o = o .. name.." = {" .. nl
    for i,vec in ipairs(tab) do
        local comma = i~=#tab and "," or ""
        o = o .. "  vec2"..tostring(vec)..comma .. nl
    end
    ]]--
    return  o .."}" .. nl .. nl
end

All the good bits are commented out. I think if we change “vec2” to “vec4” and let it run it might be interesting:

function printTable(name, tab)
    local o = ""
    o = o .. name.." = {" .. nl
    for i,vec in ipairs(tab) do
        local comma = i~=#tab and "," or ""
        o = o .. "  vec4"..tostring(vec)..comma .. nl
    end
    return  o .."}" .. nl .. nl
end

One interesting fact. Since the previous runs wrote }}}} to the output, the Converted tab wouldn’t compile so I couldn’t run again until I fixed it. Now it looks like this:

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

And the little guys draw correctly. I call this done.

10:31 AM

How about that? It wasn’t too painful, was it? Let’s think about it:

Summing Up

I think the biggest learning here is that I didn’t go far enough with my conversion, all the way to the best form for the drawing code. For one reason or another, my head was stuck in the 2d vector form, where a form suitable to the line function would have been better.

And of course, the benefit of another pair of eyes is always there. It’s just a benefit that I don’t always have in “these uncertain times”. So I’m grateful to Dave for the tip.

Once we decided to do the 4d conversion rather than just use Dave’s vectors, we chose to delete all our conversion code and create new. That was likely wise, because such things are always kind of grubby and changing them to do something completely different could be trouble.

I started with TDD, but quit TDDing as soon as something seemed to work. (I’m saying I now because what happened was my fault, you were just watching and probably saying “Oh he’s in trouble now”.)

Creating the conversion function was a pretty big bite of code, but I had it nearly right the first time.

Except for the first bug, which was obvious … and the second one, which was as bit less so.

I might have been wise to start over but I felt I was on the right track and pushed on. A more conservative approach might have been to start over on the convertTable, but it seemed worth a couple of quick tries to make it work. And it did work.

Now if I were here showing you how some perfect god-like programmer works, always selecting just the right tool from the bench and crafting perfect god-like code, I’d have gone another way.

If that had gone badly, I’d have taken my lumps and written about how badly it went. So give me this one: I got away with it.

And that’s how it feels to me. I got away with it. I took a shortcut, took a chance, and squeaked by. I bent my principles, and still wound up with a decent result. About the only thing I didn’t do wrong was eat one of the donut holes that I know are out in the kitchen.

Maybe later.

One more thing. Dave’s drawing version scales the vectors themselves rather than using the Codea scale and strokeWidth as I do. That’s an interesting choice, since to get a reasonable line width under a scale other than 1, you have to adjust the stroke width. He avoids that by just moving further, scaling thevec4 elements rather than calling scale. We’ll see about that as time goes on. I have to let that idea perk in my “mind”.

Overall, I’d be more comfortable with another test or two, but in fact I am confident that the converter works as advertised, so I’m calling it good.

Go now, and sin no more (than I do). See you next time!