Finishing up the second roulette version
21 or 22 hours later …
I believe Tozier will be joining me today. Be that as it may, I’m heading back into the second version of roulette. And I have a third version in mind already. But first we must digress.
A wild digression appears!
On the forum, I was asked why I elected to use the random number generator directly, and why I used ElapsedTime directly. A similar question came up on Twitter as well. Here’s why:
There are two reasons, at least, why I do things like that.
First, as I write these things, I like to discover what happens when we don’t play by all the rules. What happens when we hook directly to a global variable? What happens when we write a big method? What happens when our class mushes three ideas together? Do we die? Can we recover? Does it make much difference? If we don’t abstract something in advance, can we abstract it later? What happens if we move to an abstraction too soon? I want to see, and to help you see, what difference these practices make, and to discover when they matter and when they don’t.
Second, I enjoy programming “close to the metal”, especially in languages like Codea Lua, which are small and favor tight code. So, surely, some of what I do in these little exercises is conditioned by the kind of code I like to write.
Overall, I think that all these practices are there to be used, absorbed, mastered, and then forgotten. As Charlie Parker put it, “Master your instrument, master the music, then forget all that shit and just play.”
To the codemobile, Robin!
Tozier is here and up to speed. Let’s see what this program needs. Here’s the code:
RouletteWheel = class()
function RouletteWheel:init()
self.slot = 36
end
function RouletteWheel:draw()
end
function RouletteWheel:randomSlot()
return math.random(0,36)
end
function RouletteWheel:spin(aTime)
self.startSpin = aTime
self.slot = nil
self.bounceSlot = nil
self.bounceTime = aTime
end
function RouletteWheel:action(aTime)
local proposedSlot
if (aTime - self.startSpin) >= 20 then
self.slot = self:randomSlot()
end
if (aTime - self.bounceTime) >= 0.1 then
repeat
proposedSlot = self:randomSlot()
until proposedSlot ~= self.bounceSlot
self.bounceSlot = proposedSlot
self.bounceTime = aTime
end
end
We’ll look at the tests in a moment. Tozier asks whether “action” is a good name for a method. I tell him we’ll action that concern later. But it probably isn’t. Perhaps more interesting is the fact that even after the final slot is calculated, the bouncing will continue. Also interesting is the fact that the action method does two things and it’s written out longhand. We should refactor it. Let’s look at the tests and then see what we think here in the cold light of day:
function testRoulette()
CodeaUnit.detailed = false
_:describe("Roulette Tests", function()
_:test("hookup", function()
_:expect("three").is("three")
end)
_:test("wheel exists", function()
local wheel = RouletteWheel()
_:expect(wheel.slot).is(36)
end)
_:test("random slot", function()
local wheel = RouletteWheel()
local atLeastZero = true
local under37 = true
for i=0, 10000 do
local slot = wheel.randomSlot()
atLeastZero = atLeastZero and slot >= 0
under37 = under37 and slot < 37
end
_:expect(atLeastZero).is(true)
_:expect(under37).is(true)
end)
_:test("has value when done", function()
local wheel = RouletteWheel()
local startTime = 21 -- random time of day
wheel:spin(startTime)
wheel:action(startTime + 2)
_:expect(wheel.slot).is(nil)
wheel:action(startTime + 19)
_:expect(wheel.slot).is(nil)
wheel:action(startTime + 20)
_:expect(wheel.slot).isnt(nil)
end)
_:test("bounces a lot", function()
local wheel = RouletteWheel()
local startTime = 151
local tiny = 0.05
local enough = 0.101
local timeNow = startTime
wheel:spin(startTime)
wheel:action(timeNow + tiny)
_:expect(wheel.bounceSlot).is(nil)
wheel:action(timeNow + enough)
local slotNow = wheel.bounceSlot
_:expect(wheel.bounceSlot).isnt(nil)
end)
end)
end
The “bounces a lot” test could use enhancement, because I stopped after testing one bounce. That said, I’m confident that it continues to bounce, so I don’t feel a great desire to push another pair of checks in there. “Should” be done. Not gonna do it, at least not right now.
Looking at our state, I think we are close enough to done that we could wire our wheel up to the display. There’s refactoring to do but I’d like to see it work. Should we write another test, first? Well, maybe. We’re leaning on TDD today after all. How about a test to see whether it is going to display the correct string, “Bouncing nnn” or “Paying nnn” or whatever?
_:test("has correct string", function()
local wheel = RouletteWheel()
local startTime = 4
wheel:spin(startTime)
wheel:action(startTime + 0.001)
local displayString = wheel:displayString()
local match = string.match(displayString, "Bouncing: %d+")
_:expect(match).isnt(nil)
end)
The test uses the Codea Lua string.match
function, which offers a decent pattern-matching capability. Good enough for our purposes.
As you can see, I decided to look for the bouncing first. This turns out not to be the best idea, because the bounce state is dynamic and the paid-off state is static, so we might have done better doing payoff first. Anyway we tried this implementation:
function RouletteWheel:displayString()
if self.bounceSlot then return "Bouncing: " .. self.bounceSlot end
end
Yes, well this fails, because the test uses a tiny interval less than the bounce step of 0.1 seconds. This makes me hate the idea of using nil in unset values. Remember earlier how I said I don’t like that? This is an example of why. Anyway let’s change the test to use a time bigger than a tenth and less than 20.
_:test("has correct string", function()
local wheel = RouletteWheel()
local startTime = 4
wheel:spin(startTime)
wheel:action(startTime + 7.9)
local displayString = wheel:displayString()
local match = string.match(displayString, "Bouncing: %d+")
_:expect(match).isnt(nil)
end)
Test runs OK. Now for the payoff part of the test:
_:test("has correct string", function()
local displayString
local match
local wheel = RouletteWheel()
local startTime = 4
wheel:spin(startTime)
wheel:action(startTime + 7.9)
displayString = wheel:displayString()
match = string.match(displayString, "Bouncing: %d+")
_:expect(match).isnt(nil)
wheel:action(startTime + 20)
displayString = wheel:displayString()
match = string.match(displayString, "Paying: %d+")
_:expect(match).isnt(nil)
end)
And the code:
function RouletteWheel:displayString()
if self.slot then
return "Paying: " .. self.slot
elseif self.bounceSlot then
return "Bouncing: " .. self.bounceSlot
else
return "SPINNING"
end
end
Oops, I wrote some code that wasn’t required by my test, the else clause. Reading the code told me I needed an else clause, so I put it in. So shoot me. I could have made a note, written the test and then gone back and done this. However, I will go put the additional check in:
_:test("has correct string", function()
local displayString
local match
local wheel = RouletteWheel()
local startTime = 4
wheel:spin(startTime)
wheel:action(startTime + 0.001)
displayString = wheel:displayString()
match = string.match(displayString, "SPINNING")
_:expect(match).isnt(nil)
wheel:action(startTime + 7.9)
displayString = wheel:displayString()
match = string.match(displayString, "Bouncing: %d+")
_:expect(match).isnt(nil)
wheel:action(startTime + 20)
displayString = wheel:displayString()
match = string.match(displayString, "Paying: %d+")
_:expect(match).isnt(nil)
end)
This runs. Now, please let’s wire it up to the display.
As we gather our energy for that, I note that this implementation is taking fully twice as long as the first one. There have been fewer missteps, perhaps due to the testing, perhaps due to the learning from the first time. But I expect a second time to take less time than the first. (Except for second system syndrome?) Tozier reports that second times often go slower for him. I can only conclude that he’s aging.
Whatever the reason, this is a bit troubling. Anyway I’m ready to hook the wheel into the display. It goes like this:
-- R2oulette
function setup()
Wheel = RouletteWheel()
end
function touched()
Wheel:spin(ElapsedTime)
end
function draw()
Wheel:action(ElapsedTime)
background(40, 40, 50)
strokeWidth(2)
translate(WIDTH/2, HEIGHT/2)
text(Wheel:displayString(), 0, 0)
end
This crashes! The times aren’t set until we do the first spin. We could change the wheel, but Tozier asks whether we might create the wheel on the touch. That makes sense to me. So we try this:
-- R2oulette
function setup()
end
function touched(ignored)
Wheel = RouletteWheel()
Wheel:spin(ElapsedTime)
end
function draw()
if Wheel then Wheel:action(ElapsedTime) end
background(40, 40, 50)
strokeWidth(2)
translate(WIDTH/2, HEIGHT/2)
if Wheel then text(Wheel:displayString(), 0, 0) end
end
Note that in draw()
, now we have to check to see if there even is a Wheel. We hate this but having started down the create-on-touch path, I wanted to complete it even though I am sure this conditional draw is a hack.
We also found a bug, as shown in the video below: after 20 seconds, every time we call action, we compute a new slot. We need a test to be sure the slot doesn’t change after 20 seconds, until a new spin. (There is also the fact that the bounce is still running: it’s just masked because we never look at it.)
VIDEO TBD
Looking forward at the design, I’m inclined to adjust so that the Wheel initializes not spinning and with the ball in a slot. My intuition (experience) makes me think that will clean things up. But first, our test:
_:test("has correct string", function()
local displayString
local match
local wheel = RouletteWheel()
local startTime = 4
wheel:spin(startTime)
wheel:action(startTime + 0.001)
displayString = wheel:displayString()
match = string.match(displayString, "SPINNING")
_:expect(match).isnt(nil)
wheel:action(startTime + 7.9)
displayString = wheel:displayString()
match = string.match(displayString, "Bouncing: %d+")
_:expect(match).isnt(nil)
wheel:action(startTime + 20)
displayString = wheel:displayString()
match = string.match(displayString, "Paying: %d+")
_:expect(match).isnt(nil)
wheel:action(startTime + 21)
_:expect(displayString).is(wheel:displayString())
end)
This could pass by accident, even if we didn’t stop the continual paying loop. But it usually works as expected, and that’s good enough for now. Let’s change our implementation. This isn’t quite a refactoring, but I don’t expect to break any tests … and it works:
I think we’re done. Here’s what we did:
--# Main
-- R2oulette
function setup()
Wheel = RouletteWheel()
end
function touched(ignored)
Wheel:spin(ElapsedTime)
end
function draw()
Wheel:action(ElapsedTime)
background(40, 40, 50)
strokeWidth(2)
translate(WIDTH/2, HEIGHT/2)
text(Wheel:displayString(), 0, 0)
end
--# RouletteWheel
RouletteWheel = class()
function RouletteWheel:init()
self.slot = 36
self.spinning = false
end
function RouletteWheel:draw()
end
function RouletteWheel:randomSlot()
return math.random(0,36)
end
function RouletteWheel:spin(aTime)
self.spinning = true
self.startSpin = aTime
self.slot = nil
self.bounceSlot = nil
self.bounceTime = aTime
end
function RouletteWheel:action(aTime)
local proposedSlot
if not self.spinning then return end
if (aTime - self.startSpin) >= 20 then
self.slot = self:randomSlot()
self.spinning = false
end
if (aTime - self.bounceTime) >= 0.1 then
repeat
proposedSlot = self:randomSlot()
until proposedSlot ~= self.bounceSlot
self.bounceSlot = proposedSlot
self.bounceTime = aTime
end
end
function RouletteWheel:displayString()
if not self.spinning then
return "Paying: " .. self.slot
elseif self.bounceSlot then
return "Bouncing: " .. self.bounceSlot
else
return "SPINNING"
end
end
--# TestRoulette
function testRoulette()
CodeaUnit.detailed = false
_:describe("Roulette Tests", function()
_:test("hookup", function()
_:expect("three").is("three")
end)
_:test("wheel exists", function()
local wheel = RouletteWheel()
_:expect(wheel.slot).is(36)
end)
_:test("random slot", function()
local wheel = RouletteWheel()
local atLeastZero = true
local under37 = true
for i=0, 10000 do
local slot = wheel.randomSlot()
atLeastZero = atLeastZero and slot >= 0
under37 = under37 and slot < 37
end
_:expect(atLeastZero).is(true)
_:expect(under37).is(true)
end)
_:test("has value when done", function()
local wheel = RouletteWheel()
local startTime = 21 -- random time of day
wheel:spin(startTime)
wheel:action(startTime + 2)
_:expect(wheel.slot).is(nil)
wheel:action(startTime + 19)
_:expect(wheel.slot).is(nil)
wheel:action(startTime + 20)
_:expect(wheel.slot).isnt(nil)
end)
_:test("bounces a lot", function()
local wheel = RouletteWheel()
local startTime = 151
local tiny = 0.05
local enough = 0.101
local timeNow = startTime
wheel:spin(startTime)
wheel:action(timeNow + tiny)
_:expect(wheel.bounceSlot).is(nil)
wheel:action(timeNow + enough)
local slotNow = wheel.bounceSlot
_:expect(wheel.bounceSlot).isnt(nil)
end)
_:test("has correct string", function()
local displayString
local match
local wheel = RouletteWheel()
local startTime = 4
wheel:spin(startTime)
wheel:action(startTime + 0.001)
displayString = wheel:displayString()
match = string.match(displayString, "SPINNING")
_:expect(match).isnt(nil)
wheel:action(startTime + 7.9)
displayString = wheel:displayString()
match = string.match(displayString, "Bouncing: %d+")
_:expect(match).isnt(nil)
wheel:action(startTime + 20)
displayString = wheel:displayString()
match = string.match(displayString, "Paying: %d+")
_:expect(match).isnt(nil)
wheel:action(startTime + 21)
_:expect(displayString).is(wheel:displayString())
end)
end)
end
Summing up
We added self.spinning
just so that we could set up once and for all and have the display work unconditioned. The fact that we start with 36 is sort of noteworthy but our original spec was silent on that so we feel this is as good as anything, at least until the Product Owner sees it.
I’d have to say that mating the Wheel up to Codea’s draw
function was a bit awkward. It felt to me as if we had to file on it a bit, and bang it with a small hammer to plug it in. Possibly we “should” have mated it up sooner and observed the game end to end, rather than relying entirely on our tests. Continuous integration, one might call that idea. Oh, I guess we didn’t just invent that right now.
I felt less uncertainty this time than last time and had far fewer incidents of debugging. But it has taken nearly twice as long to build it this time. Its design is a bit better, with the time and roll abstracted, but is it that much better? I’m not sure.
Tozier notes that the built-in concurrency in Codea makes it harder to think about things. I definitely agree. While our tests abstract out some of the time issues, they don’t really deal with the fact that Codea is going to draw your object sixty times a second. Where I am, as of today, is that I very much want to see my program running on the screen. That pushes me a bit away from TDD style, because once I see a problem on the screen, I have a half-century of just fixing it and moving on.
Version Three (A Special Bonus)
I decided we’d build a third version. It took us seven minutes and it looks like this:
-- R3oulette
function setup()
Spinning = false
Slot = 36
Time = 0
end
function touched(ignored)
Spinning = true
Time = ElapsedTime
end
-- This function gets called once every frame
function draw()
if Spinning and ElapsedTime - Time > 5 then
Slot = math.random(0,36)
Spinning = false
end
background(40, 40, 50)
strokeWidth(2)
translate(WIDTH/2, HEIGHT/2)
if Spinning then
text("Spinning")
else
text("Paying: " .. Slot)
end
end
We tested only by running it about four times between nothing and working. We got one error when it did arithmetic on a nil
. That caused us to initialize Time. One visual issue was that once it paid off it kept changing the paid value. That caused us to put in the Spinning and
in the draw
.
Seven minutes, and done. Less than 30 lines of code.
What does this tell us? Take a moment before reading on. What do you think?
Tentative Conclusions
Tozier pointed out that some of the ease of writing our tiny version was due to our experience with the previous version(s), and he’s surely correct. On the other hand, there are only 15 lines of non-boilerplate code in this version and you can see how two lines of code per minute might be possible for a few minutes.
What’s more interesting to me than the time to do it is that we have essentially the same functionality in 25 lines as opposed to about 150. A bit more than half that 150 lines is tests. Doing the second version with more tests resulted in fewer problems, and a design that is better separated from machine artifacts like the random number generator and the ElapsedTime magic number. From a “formal” viewpoint, Version 2 is a somewhat cleaner design than Version 1. But Version 3 is so much simpler that I’d have to give it some good design credit as well. Does that tiny version need tests at all?
Suppose we could agree that Version 3 needs no tests. Is there any way of generalizing from this tiny program to anything larger?
“Write no line of code without a failing test” is a pretty strict rule. And for me, at least, if I start without a test, it gets harder and harder to test, for two reasons. First, the code becomes harder to test, the longer it goes without testing.
Second, it’s as if i become habituated to whatever non-testing cycle I’m using. Usually, that’s code a bit then run the program, then maybe print something, then fix the program. Sometimes I still fall into the trap of just coding a lot without testing, but that’s more rare.
So if I start without testing, the more the program grows, the less likely I am to test. You may have seen me in these very articles, realizing that I need tests, not knowing how to do it, and trudging ahead without the tests that would likely isolate my problems right away.
And in the upcoming Spacewar articles you’ll find the same effect. Once I go a while without testing, it gets harder and harder to start doing it. Because I am one of those people who thinks everyone is like him, I believe that most everyone probably sees this effect: if we don’t test we become less and less likely to test even as we need it more and more.