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.

As usual, I begin with hookup:

function testRoulette()

    CodeaUnit.detailed = true
    
    _:describe("Roulette Tests", function()
        
        _:test("hookup", function()
            _:expect("two").is("three")
        end)
            
    end)
    
end

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.

First, I guess I’ll create a test requiring a RouletteWheel:

        _:test("wheel exists", function()
            local wheel = RouletteWheel()
            _:expect(wheel.slot).is(36)
        end)

I’ve decided arbitrarily that the wheel has a “slot” and that the ball rests in slot 36 at the beginning of the game. This will suffice to drive out the basics of the class:

RouletteWheel = class()

function RouletteWheel:init()
    self.slot = 36
end

function RouletteWheel:draw()
end

Tests pass. I am tempted to hook up the draw function now but let’s wait.1 How about the ability to select a random slot?

        _:test("random slot", function()
            local wheel = RouletteWheel()
            local slot = wheel.randomSlot()
            local atLeastZero = slot >= 0
            local under37 = slot < 37
            _:expect(atLeastZero).is(true)
            _:expect(under37).is(true)
        end)

This drives out the randomSlot function:

function RouletteWheel:randomSlot()
    return math.random(0,36)
end

The test runs. I’m not overly impressed by a single try, so I enhance the test to check many times:

        _: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 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()
            local wheel = RouletteWheel()
            wheel:spin(21) -- random time of day
            wheel.slot = nil
            wheel: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:

function RouletteWheel:spin(aTime)
    self.startSpin = aTime
end

function RouletteWheel:action(aTime)
    if (aTime - self.startSpin) >= 20 then
        self.slot = self:randomSlot()
    end
end

The tests run. I’m a bit troubled by banging that nil into the slot, but what if we observe that the wheel has no real slot set until the time has elapsed.2 Then we can move the nil-setting inside:

function RouletteWheel:spin(aTime)
    self.startSpin = aTime
    self.slot = nil
end

function RouletteWheel:action(aTime)
    if (aTime - self.startSpin) >= 20 then
        self.slot = self:randomSlot()
    end
end

And I clean up the test a bit as well:

        _: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)

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:

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
end

function RouletteWheel:action(aTime)
    if (aTime - self.startSpin) >= 20 then
        self.slot = self:randomSlot()
    end
end

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()
            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)

Note the 0.101 there. Why? Look at the code for action:

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

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.

Stay tuned. Here’s the code and tests for the record:

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

--# testRoutlette

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

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

  2. 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.

  3. 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.