Jim Jeffries tweeted this challenge:

Anyone fancy having a go at a coding exercise I’ve written up and giving me some feedback?

The tweet included a link to a Google Docs article, and for your convenience, here’s what it says:

Estimated Duration: 1-3 hours
Author: James Jeffries
Language(s)/stacks: Any
Summary:
A roulette wheel has numbers from 0 to 36. A spin of the wheel takes 20 seconds and leaves the ball on a random number. Design and implement a roulette wheel using TDD. Think about how using time and random numbers affects your design. Consider how much functionality each test covers and what responsibilities you are testing.

Chet and I chatted about it briefly, and didn’t see that there was much of anything to use TDD on. Seemed like a call to a random number generator and the clock. With nothing pressing, I decided to give it a go in Codea Lua for the iPad, one of my favorite little toys.

Because I wasn’t expecting much of anything to happen, I didn’t do my usual thing of writing an article in parallel with the program. Instead, I embedded comments as I went along, saying what I was thinking. I’ll include the final program below but will extract and elaborate on the comments here.

Just to get the ball rolling1, I connected to CodeaUnit and wrote this starting test of the random number generator, not to see if it was random, but to be sure I was calling it correctly:

    --[[
    Do I want to test whether I can get a random number 
    between 0 and 36?
    when called with two integer arguments, minimum and maximum, 
    math.random()
    returns a uniform pseudo-random integer 
    in the range [minimum, maximum].
    Well, truth is, I've had a lot of trouble 
    with random numbers being off by one. 
    So maybe ...
    ]]--
    
    _:test("random", function()
        local min = 100
        local max = -100
        for i = 0, 1000 do
            local r = math.random(0,36)
            if r < min then min = r end
            if r > max then max = r end
        end
        _:expect(min).is(0)
        _:expect(max).is(36)
    end)

This worked, as I had rather anticipated. But now I had the testing framework in place, so I would have less resistance to writing more tests, I hoped. The only other thing the program does that I wasn’t sure about was to check elapsed time, so as to have the ball bounce around for 20 seconds. I tried to write a test for that and wound up with this:

    _:test("elapsed time", function()
        local start = os.time()
        local elapsed = ElapsedTime
        while ElapsedTime- elapsed < 20 do
            "nothing"
        end
        local stop = os.time()
        local not19 = (stop-start) > 19
        local not21 = (stop-start) < 21
        _:expect(not19).is(true)
        _:expect(not21).is(true)
        print(stop-start)
    end)

That test loops forever. It took me a long time to decide that was what was happening, because Codea never gets to the draw page at all for some reason. Anyway I finally figured out that what was going on what that since the test runs in a single draw cycle, ElapsedTime isn’t updated: it’s only updated on each draw cycle, since that’s really the only time you’re supposed to be running.

Well that was boring, and I can see no way to test the ElapsedTime feature in a unit test. Just to be ornery, I wrote this into the Main draw function:

    function draw()
        background(40, 40, 50)
        strokeWidth(2)
        local s = string.format("%f", ElapsedTime)
        text(s, 100,100)
    end

As expected, it displays an increasing float on the screen, which seems to add 1 to the integer part approximately every second.

I was tired of screwing around and decided just to code up a RouletteWheel class.

--# Main
-- Roulette

function setup()
    Wheel = RouletteWheel()
    parameter.action("Spin", function()
        Wheel:spin()
    end)
end

function draw()
    background(40, 40, 50)
    strokeWidth(2)
    Wheel:draw()
end

RouletteWheel = class()

function RouletteWheel:init()
    self.slot = 0
    self.bouncing = false
end

function RouletteWheel:draw()
    local display = "Paying: %d"
    if self.bouncing then
        self:bounce()
        display = "Bounce: %d"
    end
    local show = string.format(display, self.slot)
    text(show, 500, 500)
end

function RouletteWheel:spin()
    print("spin")
    self.bouncing = true
    self.bounceStart = ElapsedTime
    self.spinStart = ElapsedTime
end

function RouletteWheel:bounce()
    if ElapsedTime - self.bounceStart < 0.1 then return end
    self.bounceStart = ElapsedTime
    self.slot = math.random(0,36)
    if ElapsedTime - self.spinStart > 5 then self.bouncing = false end
end

Basically this just worked. The only real problem I had was that I coded the Spin button wrong, and it called spin() right away instead of waiting. For some reason, which I didn’t look into, the game then seemed to run and never stop. Fixing the parameter setup caused the program to work correctly.

Looking at the code, I can see some complexity there that could have gone wrong, although it happens that it didn’t. The time handling is a bit tricky with ElapsedTime - self.bounceStart and all that. One could have done the subtract backwards, gotten the comparisons wrong, used the wrong variables.

I guess if I wanted to test those, I’d have to call wheel:spin() and wheel:draw() from my tests, and then interrogate the values of the various member variables and functions. Maybe I should have done that.

Bottom line, I didn’t do much testing, and I didn’t get in much trouble. The trouble I did get into mostly seemed like tests wouldn’t have helped. But I’m not sure, so I guess I’ll do the exercise again soon and then try to figure out what I like and don’t like about the two ways of proceeding. I’ll try not to remember what I did here, and probably I’ll do it another way just because of the testing.

Stay tuned!


Here’s all the code just in case you want to see it as a unit.

--# Main
-- Roulette

--[[
Jim Jeffries exercise:
Estimated Duration: 1-3 hours
Author: James Jeffries
Language(s)/stacks: Any
Summary:
A roulette wheel has numbers from 0 to 36. 
A spin of the wheel takes 20 seconds 
and leaves the ball on a random number.
Design and implement a roulette wheel using TDD. 
Think about how using time and random numbers affects your design. 
Consider how much functionality each test covers 
and what responsibilities you are testing.
]]--

function setup()
    Wheel = RouletteWheel()
    parameter.action("Spin", function()
        Wheel:spin()
    end)
end

function draw()
    background(40, 40, 50)
    strokeWidth(2)
    Wheel:draw()
end

--# RouletteTest
CodeaUnit.detailed = true

_:describe("Roulette Tests", function()
    
    _:test("hookup", function()
        _:expect("two").is("three")
    end)
    
    _:test("random", function()
        local min = 100
        local max = -100
        for i = 0, 1000 do
            local r = math.random(0,36)
            if r < min then min = r end
            if r > max then max = r end
        end
        _:expect(min).is(0)
        _:expect(max).is(36)
    end)

    --[[ can't run this, loops forever
    _:test("elapsed time", function()
        local start = os.time()
        local elapsed = ElapsedTime
        while ElapsedTime- elapsed < 20 do
            "nothing"
        end
        local stop = os.time()
        local not19 = (stop-start) > 19
        local not21 = (stop-start) < 21
        _:expect(not19).is(true)
        _:expect(not21).is(true)
        print(stop-start)
    end)
    ]]--
    
-- RouletteWheel

RouletteWheel = class()

function RouletteWheel:init()
    self.slot = 0
    self.bouncing = false
end

function RouletteWheel:draw()
    local display = "Paying: %d"
    if self.bouncing then
        self:bounce()
        display = "Bounce: %d"
    end
    local show = string.format(display, self.slot)
    text(show, 500, 500)
end

function RouletteWheel:spin()
    print("spin")
    self.bouncing = true
    self.bounceStart = ElapsedTime
    self.spinStart = ElapsedTime
end

function RouletteWheel:bounce()
    if ElapsedTime - self.bounceStart < 0.1 then return end
    self.bounceStart = ElapsedTime
    self.slot = math.random(0,36)
    if ElapsedTime - self.spinStart > 5 then self.bouncing = false end
end
  1. Sorry.