Roulette Challenge
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
-
Sorry. ↩