Roulette, this time with more tests.

Other examples …

A couple of folks have decided to try the exercise. The ones I remember are DanielMarlo, here, and Dave Hounslow, here. Check them out. And if you’ve done a version, let me know and I’ll add a link.

Moving right along …

In yesterday’s article, I described doing the roulette exercise proposed by Jim Jeffries. It went OK but I could see things that could have gone wrong, where tests might have helped. And possibly, we might have come up with a better design. So my plan today is to do it again.

Chet and I do this habitually, because often our environment is broken and no tests will run. So we check first that tests work. This is particularly useful with CodeaUnit, because its syntax is a bit different and I sometimes mess it up. The hookup test is about as quick a check as we can devise.

Sure enough, “two” isn’t “three”. Turns out that’s a bug in the test so I fix it to expect that “three” is “three” and the test runs. Now let’s get to work.

I’ll use the same basic spec as yesterday. The final functionality should have a “Spin” button on the screen, and when it is pressed, the 20-second spin of the wheel begins. Ten times a second, the screen should display “Bounce: 31”, or wherever the ball is then. When the 20 seconds is up, the screen should display “Paying: 22”, or whatever the final ball position is, signifying that the croupier will pay off on 22.

All this will happen because our Main will create a RouletteWheel, and in every draw cycle, it will tell the wheel to draw itself. What is there to test? In the previous version, I felt that the code to decide whether to bounce, every 0.1 seconds, and the code to decide to stop bouncing and pay off, after 20 seconds, was a bit tricky, so maybe we should test it. My excuse for not testing with CodeaUnit last time was that ElapsedTime is only updated on the draw cycle, so I “couldn’t” get at it. Well, it’s true that I couldn’t get at ElapsedTime usefully but that doesn’t mean I couldn’t test. As written, the old code is tightly tied to the ElapsedTime magic variable. Let’s see if we can pass in a time and use it in testing.

Test runs. I double-checked it by setting the zero to 5 and the 37 to 36 and both branches of the test fail, so I’m confident that my little function runs. Note, by the way, that randomSlot doesn’t set the wheel’s slot, it just computes a new slot randomly. I wonder if we’ll even need a final slot? Probably, because the wheel will get drawn many times after the spin stops.

Now what about stopping and bouncing? I deem having a value on stopping to be the more important of those two features, so I’ll test that first. First the test. The question in my mind right now is how I’ll know whether the wheel has stored a value. I think in order to know that I’ll have to set it to an impossible value and check that it doesn’t end up that way. That’s a bit troubling, but here goes:

_:test("has value when done",function()localwheel=RouletteWheel()wheel:spin(21)-- random time of daywheel.slot=nilwheel:action(22)-- a bit later_:expect(wheel.slot).is(nil)wheel:action(40)-- a lot later_:expect(wheel.slot).is(nil)wheel:action(41)-- 20 seconds after start_:expect(wheel.slot).isnt(nil)end)

So my idea here is that the spin function will take the “ElapsedTime” when the spin starts and remember it, and the action function saves a new slot value when the time is at least 20 seconds later. Here’s the code for the wheel:

_:test("has value when done",function()localwheel=RouletteWheel()localstartTime=21-- random time of daywheel: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)

Now let’s think about that setting spin to nil when we spin. We’re essentially saying that the slot is undefined during the spin. I’m not generally comfortable using nil in that fashion but I think here it is appropriate. I think I’ll push forward with that notion and I’m planning, tentatively, to do the same kind of thing with a bounce slot.3

We could even do something cute with the display if we wanted to, but let’s stick to yesterday’s spec for now, the better to get a comparison.

Speaking of comparison, what I’m noticing here is that, so far, our RouletteWheel isn’t bound to ElapsedTime at all. It expects to have the current time passed into it. That will make calling it a bit more complex, perhaps, but it’s almost certainly worth it to uncouple our code from a magical variable. So I think this design is a bit better. To help you decide, here’s the whole class:

Now what about the bounce slot idea? We want to display the bounce slot all the time as the wheel “spins”, with the value changing every tenth of a second, when the ball momentarily hits another slot. (This is not how real roulette wheels work, of course. In those, the ball never really seems to be in a slot until the end. I just added that feature to make the problem a little more interesting. We’ll stick with it.)

Now if the bounce slot were truly random, it could take on the same value on two or more consecutive bounces. That will make testing tricky, because we can’t just check that the bounce value isn’t the same: it could be, and our test will break. We could mash it back to nil from outside, after testing it for non-nil, or we could enhance our spec to say that the bounce slot is never the same twice in a row. I’m inclined to go that way. Let’s try to write a test:

_:test("bounces a lot",function()localwheel=RouletteWheel()localstartTime=151localtiny=0.05localenough=0.101localtimeNow=startTimewheel:spin(startTime)wheel:action(timeNow+tiny)_:expect(wheel.bounceSlot).is(nil)wheel:action(timeNow+enough)localslotNow=wheel.bounceSlot_:expect(wheel.bounceSlot).isnt(nil)end)

See that test for >= 0.1? Well it turns out that (151 - 0.1) - 151 is not greater than nor equal to 0.1, because of floating point rounding. It’s true, and typical of floating point. Anyway the test runs fine now.

The repeat - until is troubling. I needed to do something so that my test was protected from a random duplication, which could have engendered a false failure. It is probably a mistake to mess up the design to support testing, though it is a good idea to improve the design to support testing. How do you know the difference? Pay attention, learn, use your improving best judgment.

Tentative non-conclusions

I’m sure this design is better, because it is decoupled from ElapsedTime, and it has the random number generator abstracted inside a method. It has some odd bits but it’s not bad. I see some refactoring that needs doing, and the wheel is still not wired up to display.

However! It’s time for lunch. This makes me think doing it this way has taken longer. In any case, I’ll finish up the effort, and this article, next time.

It turns out that hooking up the draw function might have been a good idea. See the remarks under “Summing up”. ↩

It turns out that banging in the nil was probably a mistake too. Note that in the final version, including the display, that is mostly backed out. ↩

I think my intuition was correct here. A flag value like the nil is almost never a good idea even when it is a good idea. We could have done something tricky, like a promise object or something, but that would be well over the top. Lesson learned: there’s hackery and there’s hackery. This was the bad kind. ↩