Bowling Rube
Today I’ll try to make my new “holes” scheme work. Wish me luck.
I think I’ll start with tests and code for the new Hole object, based on yesterday’s “spec”, not without thinking further, mind you.
First test:
function Tests:test_hole_creation()
local hole = Hole("open", "closed")
self:assert_equals(hole.top_lid, "open")
self:assert_equals(hole.bottom_lid, "closed")
end
That passes, given this:
local Hole = class()
function Hole:init(top, bot)
self.top_lid = top
self.bottom_lid = bot
end
OK, so today’s version of the Hole spec is
- It responds to
roll(frame, pins)
, a pointer to the frame it’s in and the number of pins dropped. - If its top lid is closed it returns pins with no other action.
- It closes its top_lid.
- It calls back to frame to add pins to the frame_count. Frame, I think, should reply with the new total.
- If the total is ten, call back to the frame to open the next two holes top and bottom.
- If the bottom is open return pins, otherwise return nil.
I’m sure enough of that to implement it and then fix up frame later. This is not in accord with my usual practice of getting the new code in play as soon as possible. We’ll see how it goes. I might even change my mind, I frequently do, based on events or whim.
Second test:
function Tests:test_hole_closed()
local hole = Hole("closed", "closed")
local continue = hole:roll(nil, 9)
self:assert_equals(continue, 9)
end
And passes with this:
function Hole:roll(frame, pins)
if self.top_lid == "closed" then
return pins
end
end
So far so good. Now I have a bit of an issue because on our next move or if i delay it, the one after, we need to call back to our frame. Our frame is not yet ready for this, I cold create a new Frame object, create some kind of test dummy, or begin modifying the existing Frame. I prefer the latter, so long as I don’t have to break it. Let’s try.
function Tests:test_hole_open_closes_lid()
local hole = Hole("open", "closed")
local continue = hole:roll(nil, 9)
self:assert_equals(hole.top_lid, "closed")
end
And, should be easy enough … after fixing two typos in a five line test …
function Hole:roll(frame, pins)
if self.top_lid == "closed" then
return pins
end
self.top_lid = "closed"
end
I should mention why I’m using strings as the indicators. I thought of naming the state top_lid_closed
and using true/false, or even having two properties, and that seemed a bit much for Lua. Class variables seem a bit much, as does inventing Enum. So here we are. So far I rather like it.
OK, I think we have to deal with the call back now, with this new test:
function Tests:test_hole_reports_score()
local frame = Frame()
local hole = Hole("open", "closed")
hole:roll(frame, 9)
self:assert_equals(frame.frame_score, 9)
end
I’m just going to give frame a new field, frame_score
and deal with the fallout later. If all the tests run, I’m OK.
I do have to change the existing tests to give them a frame instance where I had nil
. No surprise there.
function Hole:roll(frame, pins)
if self.top_lid == "closed" then
return pins
end
self.top_lid = "closed"
local total = frame:add_to_score(pins)
end
We are green. Note that I coded ahead, retaining the frame score.Also in this case we are not yet dealing with the value returned to the frame. I’ll write a test but I’m not sure how to make it work without a test dummy or something. Anyway write it:
function Tests:test_hole_returns_roll_if_open()
local frame = Frame()
local hole = Hole("open", "open")
local returned = hole:roll(frame, 7)
self:assert_equals(frame.continue_rolling, 7)
end
I’ve just invented the idea that the Frame has a member continue_rolling
that will start as the number of pins dropped, which it will learn from the game, and which will be set to nil if one of the holes holds on to the roll (i.e. is closed on the bottom).
This is a bit of a reach. I’m not certain this is going to work but I think I’m at about 80% sure. We’ll find out and learn something even if it’s not quite the thing.
No. I’m wrong! I can just check the return from Hole:roll
. We are responsible for returning the value. When we start changing Frame, we’ll deal with using it.
function Tests:test_hole_returns_roll_if_open()
local frame = Frame()
local hole = Hole("open", "open")
local returned = hole:roll(frame, 7)
self:assert_equals(returned, 7)
end
I think this fails returning nil. Test it.
[07:32] Tests: Actual: nil, Expected: 7. Tests:test_hole_returns_roll_if_open
Perfect. Fix it. I’ll code one test ahead. No, I’ll write the other test and fix them both in one go. Getting fancy here, bunko.
function Tests:test_hole_returns_nil_if_closed()
local frame = Frame()
local hole = Hole("open", "closed")
local returned = hole:roll(frame, 7)
self:assert_equals(returned, nil)
end
Should succeed for now, since default return is nil. Fix:
function Hole:roll(frame, pins)
if self.top_lid == "closed" then
return pins
end
self.top_lid = "closed"
local total = frame:add_to_score(pins)
if self.bottom_lid == "closed" then
return nil
else
return pins
end
end
Now we only need to deal with the request to frame to open the bonus frames.
function Tests:test_mark_opens_two_holes()
local frame = Frame()
local hole = Hole("open", "closed")
hole:roll(frame, 10)
self:assert_equals(frame.holes[2].top_lid, 'open')
self:assert_equals(frame.holes[2].bottom_lid, 'open')
self:assert_equals(frame.holes[3].top_lid, 'open')
self:assert_equals(frame.holes[3].bottom_lid, 'open')
end
This test posits that Frame has at least three holes. And that the latter two end up open/open.
I think we need to assert the initial state of the holes as well, and, of course, add them to Frame.
function Tests:test_mark_opens_two_holes()
local frame = Frame()
local hole = Hole("open", "closed")
self:assert_equals(frame.holes[1].top_lid, 'open')
self:assert_equals(frame.holes[2].bottom_lid, 'closed')
self:assert_equals(frame.holes[2].top_lid, 'open')
self:assert_equals(frame.holes[2].bottom_lid, 'closed')
self:assert_equals(frame.holes[3].top_lid, 'closed')
self:assert_equals(frame.holes[3].bottom_lid, 'open')
hole:roll(frame, 10)
self:assert_equals(frame.holes[2].top_lid, 'open')
self:assert_equals(frame.holes[2].bottom_lid, 'open')
self:assert_equals(frame.holes[3].top_lid, 'open')
self:assert_equals(frame.holes[3].bottom_lid, 'open')
end
The initial state, according to my plan, has the first two holes open on top and closed on the bottom, to grab the two rolls of an open frame or spare. The third hole is closed, assuming no bonus roll, and open at the bottom because we might as well set it to the state it’ll have when a bonus is available.
The test fails miserably, there being no holes in the Frame. We init them:
local Frame = class()
function Frame:init()
self.rolls = {}
self.frame_score = 0 -- for Holes
self.holes = {}
table.insert(self.holes, Hole("open", "closed"))
table.insert(self.holes, Hole("open", "closed"))
table.insert(self.holes, Hole("closed", "open"))
end
The first part of the test is probably passing but I can’t tell which test is which. Add messages. Yes that assures me that the initials are working and the right ones are failing below.
Now to call back to frame asking it to do stuff. I was going to have the Holes specify the details but we only have the one case, which is to set all remaining holes open at top and bottom. I’m not sure how to deal with the “remaining” notion but for now this should pass:
function Hole:roll(frame, pins)
if self.top_lid == "closed" then
return pins
end
self.top_lid = "closed"
local total = frame:add_to_score(pins)
if total == 10 then
frame:set_remaining_holes_open()
end
if self.bottom_lid == "closed" then
return nil
else
return pins
end
end
function Frame:set_remaining_holes_open()
self.holes[2].top_lid = "open"
self.holes[2].bottom_lid = "open"
self.holes[3].top_lid = "open"
self.holes[3].bottom_lid = "open"
end
This is a bit naff, touching the Hole’s lids, but I think we’ll allow it, After a bit of alignment of intention, test, and code, this passes:
function Tests:test_mark_opens_two_holes()
local frame = Frame()
local hole = Hole("open", "closed")
self:assert_equals(frame.holes[1].top_lid, 'open', "i1t")
self:assert_equals(frame.holes[2].bottom_lid, 'closed', "i1b")
self:assert_equals(frame.holes[2].top_lid, 'open', "i2t")
self:assert_equals(frame.holes[2].bottom_lid, 'closed', "i2b")
self:assert_equals(frame.holes[3].top_lid, 'closed', "i3t")
self:assert_equals(frame.holes[3].bottom_lid, 'open', "i3b")
hole:roll(frame, 10)
self:assert_equals(frame.holes[2].top_lid, 'open', "2t")
self:assert_equals(frame.holes[2].bottom_lid, 'open', "2b")
self:assert_equals(frame.holes[3].top_lid, 'open', "3t")
self:assert_equals(frame.holes[3].bottom_lid, 'open', "3b")
end
With this code:
local Frame = class()
function Frame:init()
self.rolls = {}
self.frame_score = 0 -- for Holes
self.holes = {}
table.insert(self.holes, Hole("open", "closed"))
table.insert(self.holes, Hole("open", "closed"))
table.insert(self.holes, Hole("closed", "open"))
end
function Frame:set_remaining_holes_open()
self.holes[2].top_lid = "open"
self.holes[2].bottom_lid = "open"
self.holes[3].top_lid = "open"
self.holes[3].bottom_lid = "open"
end
I need a break and this is a good spot. Let’s sum up.
Summary
I was a bit uncertain about starting with the Hole class and mostly working there, as what we did was entirely speculative. I have a theory that this object , properly used, will let us score bowling in a very interesting way. We’ve worked for an hour and a half and have no real verification that the idea is going to work. So this whole exercise is a bit of a spike, and it’s taking longer than I’d like to get feedback on whether it will work at all.
I remain confident that it will. I predict that we’ll plug it in place in Frame and all our many existing scoring tests will continue to run. (There may be some frame detail tests in there that will have to go. We’ll see.)
Next steps will be to get frame to deal with the notion of remaining
when that call is made from the second hole, which it might be. That’s OK … the frame needs to offer each roll to every hole until it is consumed … and if this frame is satisfied, we’ll use the same roll in the next frame and the next and so on.
I think this is OK. We’ll see … next time!