Bowling, of Course. Trying iLua for Ipad.
I've downloaded the iLuaBox app for my iPad. This is a small but robust implementation of core Lua. Very interesting!
It’s almost true that there’s no way to do programming directly on the iPad. The only exception I’ve found is iLuaBox from MobileAppSystems. Naturally, I had to try it. My head is clear enough from my recent knee replacement that I thought I could probably learn it. (It’s not so clear that I think I can learn J, but I’m beginning to wonder if it ever was. :) I’ll return to J later.)
Naturally, after just a bit of the usual fumbling around with 2+2 and print, I decided to test-drive the simple bowling app that I use for most new languages I try. The problem is: “Given a list of the rolls of a legal game of bowling, compute the total score for the game.”
Chet and I use this example because it is about the right size to do a pair-programming, simple design, refactoring demonstration in 90 minutes or so. Much more and there just isn’t time to get to the end. There are other excellent bowling kata out there. This one is the one I use.
There is a LuaUnit in existence, but as this is my first real program, I decided to roll my own. It doesn’t take much, just a simple assert function. Below is my first cut. The file has some comments, and two sections, one for the actual bowling code, and one for the assert function and the tests. In the fullness of time, I’d break these things up into modules, of course. We’re just loosening up our fingers here.
-- bowling01.lua -- initial bowling and test harness. -- bowling function score(ignored) return 0 end -- assert function assertEquals(expected, actual) if expected ~= actual then print ( "expected ", expected, " was ", actual) end end zeros = {0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0,} assertEquals(0, score(zeros))
What we see here should be pretty clear. We have a simple assert function that just compares expected to actual. And we have one test. (I see now that I should have put in a comment saying – tests, but I didn’t. So be it. You get to see what I really do, warts and all.)
As always, the first test is a series of gutter balls, and the implementation is “fake it till you make it”, namely return zero. That gives me the quickest possible way of hooking things up. And, actually, I first faked the answer 99, to be sure that the assert gives a message when the results don’t match.
Once that worked, I beefed up the “framework” a bit with a beginTests function that sets some values, and an endTests function that displays the results:
-- bowling02.lua
-- initial bowling and test harness.
-- bowling
function score(ignored)
return 0
end
-- assert
function beginTests()
print "begin tests"
asserts = 0
successes = 0
failures = 0
end
function endTests()
print( asserts .. " asserts, " .. successes .. " successes " .. failures .. " failures")
end
function assertEquals(expected, actual)
asserts = asserts + 1
if expected ~= actual then
print ( "expected ", expected, " was ", actual)
failures = failures + 1
else
successes = successes + 1
end
end
beginTests()
zeros = {0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0,}
assertEquals(0, score(zeros))
endTests()
OK. That done, the next step is a series of open frames, to drive out the actual scoring. Read on and pick up further commentary below:
-- bowling03.lua
-- initial bowling and test harness.
-- bowling
function score(rolls)
total = 0
for i, roll in ipairs(rolls) do
total = total + roll
end
return total
end
-- assert
function beginTests()
print "begin tests"
asserts = 0
successes = 0
failures = 0
end
function endTests()
print( asserts .. " asserts, " .. successes .. " successes " .. failures .. " failures")
end
function assertEquals(expected, actual)
asserts = asserts + 1
if expected ~= actual then
print ( "expected " .. expected .. ", was " .. actual)
failures = failures + 1
else
successes = successes + 1
end
end
beginTests()
zeros = {0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0,}
assertEquals(0, score(zeros))
opens = {}
for frame = 1,10 do
opens[#opens+1] = 4
opens[#opens+1] = 5
end
assertEquals(90, score( opens))
endTests()
I hope the score() code is clear: just looping over the list adding it up. The opens test may need just a tiny bit of explanation. Lua arrays generally start at 1, not zero. #opens is the number of items in the table named opens. So the code opens[#opens+1] = x adds x at the end of opens. The table grows as needed to contain the new elements.
The code that just sums the rolls doesn’t reflect all my design ideas very well. My design idea was “add up the frames” but I translated that to “add up the rolls”, which gets the right answer for now. I decide to refactor now, to better reflect the need for a frame.
I happen to know that the next test I always do, spare, will break the design, forcing me to this step. Rather than break the code, back the test out, improve the code, I decide to just improve the code.
My cunning plan is this: given the array of rolls, use the first frame’s worth, the first two, summing them in. Then remove them, and loop. Now we’ll be looking at the second frame. Repeat.
In doing this, I realized that the repeated 4 5 4 5 in my test would not tell me whether I was really dropping items from the array or not. So I added another test, 4 5 3 5. As you’ll see, it works.
-- bowling04.lua
-- refactor for frames?
-- try list remove
-- bowling
function score(rolls)
total = 0
for frame = 1,10 do
total = total + rolls[1] + rolls[2]
table.remove(rolls,1)
table.remove(rolls,1)
end
return total
end
-- assert
function beginTests()
print "begin tests"
asserts = 0
successes = 0
failures = 0
end
function endTests()
print( asserts .. " asserts, " .. successes .. " successes " .. failures .. " failures")
end
function assertEquals(expected, actual)
asserts = asserts + 1
if expected ~= actual then
print ( "expected " .. expected .. ", was " .. actual)
failures = failures + 1
else
successes = successes + 1
end
end
beginTests()
zeros = {0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0,}
assertEquals(0, score(zeros))
opens = {}
for frame = 1,10 do
opens[#opens+1] = 4
opens[#opens+1] = 5
end
assertEquals(90, score( opens))
opens = {}
for frame = 1,5 do
opens[#opens+1] = 4
opens[#opens+1] = 5
opens[#opens+1] = 3
opens[#opens+1] = 5
end
assertEquals(85, score( opens))
endTests()
I see now that I shouldn’t have called the index on that second test “frame”. Remind me to change it: it represents two frames. But it’s a righteous test and it runs.
Looking at the code above, I see duplication in the table remove. In addition it’s not abstract enough for my taste. And the summing of the rolls doesn’t express my intention as well as possible: it is calculating the frame score. So I refactor again, as below:
-- bowling05.lua
-- refactor for frames
-- express frame score idea
-- remove remove dup and express better
-- bowling
function frameScore(rolls)
return rolls[1] + rolls[2]
end
function remove(rolls, n)
for i = 1,n do
table.remove(rolls,1)
end
end
function score(rolls)
total = 0
for frame = 1,10 do
total = total + frameScore(rolls)
remove(rolls,2)
end
return total
end
-- assert
function beginTests()
print "begin tests"
asserts = 0
successes = 0
failures = 0
end
function endTests()
print( asserts .. " asserts, " .. successes .. " successes " .. failures .. " failures")
end
function assertEquals(expected, actual)
asserts = asserts + 1
if expected ~= actual then
print ( "expected " .. expected .. ", was " .. actual)
failures = failures + 1
else
successes = successes + 1
end
end
beginTests()
zeros = {0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0,}
assertEquals(0, score(zeros))
opens = {}
for frame = 1,10 do
opens[#opens+1] = 4
opens[#opens+1] = 5
end
assertEquals(90, score( opens))
opens = {}
for frame = 1,5 do
opens[#opens+1] = 4
opens[#opens+1] = 5
opens[#opens+1] = 3
opens[#opens+1] = 5
end
assertEquals(85, score( opens))
endTests()
Now we have a direct expression of the frame score notion. The remove part is still pretty ad hoc: just removes two. Our intention is to remove as many rolls as there are in the frame, which is two right now. Of course I know that’s about to change, but frankly the code could use improvement anyway, as we see here:
-- bowling06.lua
-- refactor for frames
-- express frame removal
-- bowling
function frameScore(rolls)
return rolls[1] + rolls[2]
end
function remove(rolls, n)
for i = 1,n do
table.remove(rolls,1)
end
end
function removeFrame(rolls)
remove(rolls,2)
end
function score(rolls)
total = 0
for frame = 1,10 do
total = total + frameScore(rolls)
removeFrame(rolls)
end
return total
end
-- assert
function beginTests()
print "begin tests"
asserts = 0
successes = 0
failures = 0
end
function endTests()
print( asserts .. " asserts, " .. successes .. " successes " .. failures .. " failures")
end
function assertEquals(expected, actual)
asserts = asserts + 1
if expected ~= actual then
print ( "expected " .. expected .. ", was " .. actual)
failures = failures + 1
else
successes = successes + 1
end
end
beginTests()
zeros = {0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0,}
assertEquals(0, score(zeros))
opens = {}
for frame = 1,10 do
opens[#opens+1] = 4
opens[#opens+1] = 5
end
assertEquals(90, score( opens))
opens = {}
for frame = 1,5 do
opens[#opens+1] = 4
opens[#opens+1] = 5
opens[#opens+1] = 3
opens[#opens+1] = 5
end
assertEquals(85, score( opens))
endTests()
OK, now the main loop is pretty clear: loop ten frames, sum in the frame score, remove the frame. Makes sense, expresses intention. And, it happens to be really nicely factored, ready for the next step. Now I write a test for spare, and make it work:
-- bowling07.lua
-- refactor for frames
-- spare
-- bowling
function frameScore(rolls)
base = rolls[1] + rolls[2]
if base == 10 then
base = base + rolls[3]
end
return base
end
function remove(rolls, n)
for i = 1,n do
table.remove(rolls,1)
end
end
function removeFrame(rolls)
remove(rolls,2)
end
function score(rolls)
total = 0
for frame = 1,10 do
total = total + frameScore(rolls)
removeFrame(rolls)
end
return total
end
-- assert
function beginTests()
print "begin tests"
asserts = 0
successes = 0
failures = 0
end
function endTests()
print( asserts .. " asserts, " .. successes .. " successes " .. failures .. " failures")
end
function assertEquals(expected, actual)
asserts = asserts + 1
if expected ~= actual then
print ( "expected " .. expected .. ", was " .. actual)
failures = failures + 1
else
successes = successes + 1
end
end
beginTests()
zeros = {0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0,}
assertEquals(0, score(zeros))
opens = {}
for frame = 1,10 do
opens[#opens+1] = 4
opens[#opens+1] = 5
end
assertEquals(90, score( opens))
opens = {}
for frame = 1,5 do
opens[#opens+1] = 4
opens[#opens+1] = 5
opens[#opens+1] = 3
opens[#opens+1] = 5
end
assertEquals(85, score( opens))
-- spare
spare = { 4,5, 5,5, 4,5, 4,5, 4,5, 4,5, 4,5, 4,5, 4,5, 4,5}
assertEquals(95, score(spare))
endTests()
Sweet. The spare causes me to improve the frameScore method only, and leaves the rest alone. Now we’ll do strike, which should require us to improve frameScore again, plus the removeFrame function:
-- bowling08.lua
-- refactor for frames
-- strike
-- bowling
function frameScore(rolls)
base = rolls[1] + rolls[2]
if rolls[1] == 10 or base == 10 then
base = base + rolls[3]
end
return base
end
function remove(rolls, n)
for i = 1,n do
table.remove(rolls,1)
end
end
function removeFrame(rolls)
remove(rolls,frameSize(rolls))
end
function frameSize(rolls)
if rolls[1] == 10 then
return 1
else
return 2
end
end
function score(rolls)
total = 0
for frame = 1,10 do
total = total + frameScore(rolls)
removeFrame(rolls)
end
return total
end
-- assert
function beginTests()
print "begin tests"
asserts = 0
successes = 0
failures = 0
end
function endTests()
print( asserts .. " asserts, " .. successes .. " successes " .. failures .. " failures")
end
function assertEquals(expected, actual)
asserts = asserts + 1
if expected ~= actual then
print ( "expected " .. expected .. ", was " .. actual)
failures = failures + 1
else
successes = successes + 1
end
end
beginTests()
zeros = {0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0,}
assertEquals(0, score(zeros))
opens = {}
for frame = 1,10 do
opens[#opens+1] = 4
opens[#opens+1] = 5
end
assertEquals(90, score( opens))
opens = {}
for frame = 1,5 do
opens[#opens+1] = 4
opens[#opens+1] = 5
opens[#opens+1] = 3
opens[#opens+1] = 5
end
assertEquals(85, score( opens))
-- spare
spare = { 4,5, 5,5, 4,5, 4,5, 4,5, 4,5, 4,5, 4,5, 4,5, 4,5}
assertEquals(95, score(spare))
-- strike
strike = { 4,5, 10, 4,5, 4,5, 4,5, 4,5, 4,5, 4,5, 4,5, 4,5}
assertEquals(100, score(strike))
endTests()
And there we are. I haven’t put in the usual fancy tests for perfect game and alternating strikes and spares. I wanted to test my scheme for getting the code over here into the article. As you can see, it worked pretty well.
So, there you are. Bowling for Lua, done in iLuaBox on the iPad.
What’s next? Well, surely I’ll test the perfect and alternating games. I need to look at how to modularize the test framework separately from the bowling game, and similar cleanup. This will be interesting, because Lua has some nifty ways of doing that, using its table facility.
And, of course, we should try to do a more object-oriented version, which Lua supports, again with tables.
And, since Lua understands closures, maybe I should look into doing a functional programming kind of approach as well.
All in good time. This is all a bit painful, as typing code into an iPad isn’t exactly a delight. One thing at a time. Stay tuned!