Many moons I ago I wrote The Bowling Challenge, not realizing how much interest the challenge would generate, or the lack of time I was about to have to try to test them all. I won't say I'm caught up entirely, but I did start working through the responses again this weekend. I'm going to show you one response that I believe scores a game correctly, but since I took so long I'll picking another winning entry soon - I'll be looking for one that takes a different approach than this one.
To start with, if you're not a ace bowling scorer, you might some this online scoring calculator handy.
I identified the following test scenarios that I thought would do a good job of testing the implementation. If you can think of a scenario that could reveal a bug that these don't, drop me a note and I'll add it to the test suite.
- Perfect game. Bowler threw strikes in all 10 frames and on the 'extra'
ball at the end of the 10th frame. 10th frame is the worst exception case,
want to be sure we test it.
- No strikes, no spares, only score pins on first ball of each frame.
- No strikes, no spares, score on both frames.
- All spare game, no score on extra ball in 10th. Testing the margins again.
- All spare game, strike on extra ball in 10th.
- 9 spares, all strikes in last frame. Not sure this is a good test, but
doesn't hurt to test it
- Absolutely no score
I didn't test validation. We're trying to prove the algorithm, I'm assuming
that anyone who can get the algorithm to work can write the validation to check
for too many frames, more than 10 pins in a frame, etc.
To test the entry this article is based on, start by executing the following
in a test database:
CREATE TABLE [Scores] ( [int] IDENTITY (1, 1) NOT NULL , , [DF__Scores__DateAdde__76CBA758] DEFAULT (getdate()), [tinyint] NOT NULL , NOT NULL , PRIMARY KEY CLUSTERED [PRIMARY] |
Even though it's a little long, I'm going to put the code inline here - both
to make it easier to read and because I know it'll get included in the next of
our 'Best of SQLServerCentral' volumes!
create Procedure dbo.usp_ScoreBowlingGameI think my first thought on examining this solution is that isn't set@UserID int,
@Ball1 tinyint,
@Ball2 tinyint = 0,
@Ball3 tinyint = 0
AS
--Score a bowling game
Set @Ball1 = isnull(@Ball1, 0)
Set @Ball2 = isnull(@Ball2, 0)
Set @Ball3 = isnull(@Ball3, 0)
Set NoCount On
Declare @NextFrameNumber tinyint
Declare @Score smallint
Declare @FrameNumber tinyint
Declare @FrameBall1 smallint
Declare @FrameBall2 smallint
Declare @NextBall1 smallint
Declare @NextBall2 smallint
Declare @NumberOfPins smallint
Declare @RunningTotal smallint
Set @RunningTotal = 0
--calculate next frame
Select @NextFrameNumber = isnull(Max(FrameNumber), 0) + 1
From Scores with (nolock)
Where userid = @UserID
--dont allow extra frames or pin count higher than 10
if (@NextFrameNumber < 10 and @Ball1 + @Ball2 <= 10)
or (@NextFrameNumber = 10 and @Ball1 + @Ball2 + @Ball3 <= 30)
begin
--insert frame pins
insert into scores(userid, framenumber, ball1, ball2, numberofpins)
values (@UserID, @NextFrameNumber, @Ball1, @Ball2, null)
--select all frames in order
declare bowling_cursor cursor for
select framenumber, ball1, ball2, numberofpins
from scores with (nolock)
where userid = @UserID
and framenumber <= @NextFrameNumber
order by framenumber
open bowling_cursor
fetch next from bowling_cursor into @FrameNumber, @FrameBall1, @FrameBall2, @NumberOfPins
while @@Fetch_Status = 0
begin
--if frame already scored, skip
if @NumberOfPins is NULL
begin
--reset value
Set @Score = NULL
if @FrameNumber < 10
begin
--is strike
if @FrameBall1 = 10
begin
if @FrameNumber <> 9
begin
if exists(select * from scores where framenumber = @FrameNumber + 1 and userid = @UserID)
begin
Select @NextBall1 = ball1, @NextBall2 = ball2 from scores
where framenumber = @FrameNumber + 1
and userid = @UserID
--if strike
if @NextBall1 = 10
begin
if exists(select * from scores
where framenumber = @FrameNumber + 2
and userid = @UserID)
begin
Select @NextBall2 = ball1 from scores
where framenumber = @FrameNumber + 2
and userid = @UserID
Set @RunningTotal = @RunningTotal + 10 + @NextBall1 + @NextBall2
Set @Score = @RunningTotal
end
else
begin
Set @Score = Null
end
end
else
begin
Set @RunningTotal = @RunningTotal + 10 + @NextBall1 + @NextBall2
Set @Score = @RunningTotal
end
end
end
if @FrameNumber = 9
begin
if exists(select * from scores
where framenumber = @FrameNumber + 1 and userid = @UserID)
begin
Select @NextBall1 = ball1, @NextBall2 = ball2 from scores
where framenumber = @FrameNumber + 1
and userid = @UserID
Set @RunningTotal = @RunningTotal + 10 + @NextBall1 + @NextBall2
Set @Score = @RunningTotal
end
end
end
--is spare
if @FrameBall1 + @FrameBall2 = 10 and @FrameBall1 < 10
begin
if exists(select * from scores where framenumber = @FrameNumber + 1 and userid = @UserID)
begin
Select @NextBall1 = ball1 from scores where framenumber = @FrameNumber + 1 and userid = @UserID
Set @RunningTotal = @RunningTotal + 10 + @NextBall1
Set @Score = @RunningTotal
end
end
--is other
if @FrameBall1 + @FrameBall2 < 10
begin
Set @RunningTotal = @RunningTotal + @FrameBall1 + @FrameBall2
Set @Score = @RunningTotal
end
end
--if frame 10
if @FrameNumber = 10
begin
Set @RunningTotal = @RunningTotal + @Ball1 + @Ball2 + @Ball3
Set @Score = @RunningTotal
print 'Game Over'
end
if @Score is not null
begin
--update score for current frame and user
update scores set numberofpins = @Score
where framenumber = @FrameNumber and userid = @UserID
end
end
else
begin
--print 'Skipped frame: ' + cast(@FrameNumber as varchar(2)) + ', already scored.'
Select @RunningTotal = numberofpins from scores
where framenumber = @FrameNumber and userid = @UserID
end
fetch next from bowling_cursor into @FrameNumber, @FrameBall1, @FrameBall2, @NumberOfPins
end
close bowling_cursor
deallocate bowling_cursor
end
based. Not a requirement, only requirement is that the answer has to be correct!
I noted some decent validation up front to deal with nulls, pins that exceed the
allowable range. Cursor being used - note that it's not specified as local or
readonly, something easily corrected and doesn't affect the test results. I also
see that the code is readable, good variable names, and some comments, enough
for me to decipher the flow. The assumption is that the proc is called once for
each frame, passing in the number of pins for the first and second balls (and
for the third ball on the tenth frame).
I set up a new test database, created the table as noted above, created the
procedure. I then ran through the following tests.
declare @Score intset nocount on
--test perfect game
delete from scores
exec dbo.usp_ScoreBowlingGame 1, 10, 0, 0
exec dbo.usp_ScoreBowlingGame 1, 10, 0, 0
exec dbo.usp_ScoreBowlingGame 1, 10, 0, 0
exec dbo.usp_ScoreBowlingGame 1, 10, 0, 0
exec dbo.usp_ScoreBowlingGame 1, 10, 0, 0
exec dbo.usp_ScoreBowlingGame 1, 10, 0, 0
exec dbo.usp_ScoreBowlingGame 1, 10, 0, 0
exec dbo.usp_ScoreBowlingGame 1, 10, 0, 0
exec dbo.usp_ScoreBowlingGame 1, 10, 0, 0
exec dbo.usp_ScoreBowlingGame 1, 10, 10, 10
select @Score = max(numberofpins) from scores with (nolock) where userid = 1
print 'Perfect game'
if @Score = 300
print '--Passed'
else
print '--Failed'
--test all no strike, no spare game
delete from scores
exec dbo.usp_ScoreBowlingGame 1, 1, 0, 0
exec dbo.usp_ScoreBowlingGame 1, 1, 0, 0
exec dbo.usp_ScoreBowlingGame 1, 1, 0, 0
exec dbo.usp_ScoreBowlingGame 1, 1, 0, 0
exec dbo.usp_ScoreBowlingGame 1, 1, 0, 0
exec dbo.usp_ScoreBowlingGame 1, 1, 0, 0
exec dbo.usp_ScoreBowlingGame 1, 1, 0, 0
exec dbo.usp_ScoreBowlingGame 1, 1, 0, 0
exec dbo.usp_ScoreBowlingGame 1, 1, 0, 0
exec dbo.usp_ScoreBowlingGame 1, 1, 0, 0
select @Score = max(numberofpins) from scores with (nolock) where userid = 1
print 'No strike/no spare'
if @Score = 10
print '--Passed'
else
print '--Failed'
delete from scores
exec dbo.usp_ScoreBowlingGame 1, 1, 1, 0
exec dbo.usp_ScoreBowlingGame 1, 1, 1, 0
exec dbo.usp_ScoreBowlingGame 1, 1, 1, 0
exec dbo.usp_ScoreBowlingGame 1, 1, 1, 0
exec dbo.usp_ScoreBowlingGame 1, 1, 1, 0
exec dbo.usp_ScoreBowlingGame 1, 1, 1, 0
exec dbo.usp_ScoreBowlingGame 1, 1, 1, 0
exec dbo.usp_ScoreBowlingGame 1, 1, 1, 0
exec dbo.usp_ScoreBowlingGame 1, 1, 1, 0
exec dbo.usp_ScoreBowlingGame 1, 1, 1, 0
select @Score = max(numberofpins) from scores with (nolock) where userid = 1
print 'No strike/no spare, scored in all frames'
if @Score = 20
print '--Passed'
else
print '--Failed'
--test all all spare game
delete from scores
exec dbo.usp_ScoreBowlingGame 1, 1, 9, 0
exec dbo.usp_ScoreBowlingGame 1, 1, 9, 0
exec dbo.usp_ScoreBowlingGame 1, 1, 9, 0
exec dbo.usp_ScoreBowlingGame 1, 1, 9, 0
exec dbo.usp_ScoreBowlingGame 1, 1, 9, 0
exec dbo.usp_ScoreBowlingGame 1, 1, 9, 0
exec dbo.usp_ScoreBowlingGame 1, 1, 9, 0
exec dbo.usp_ScoreBowlingGame 1, 1, 9, 0
exec dbo.usp_ScoreBowlingGame 1, 1, 9, 0
exec dbo.usp_ScoreBowlingGame 1, 1, 9, 0
select @Score = max(numberofpins) from scores with (nolock) where userid = 1
print 'All spare game'
if @Score = 109
print '--Passed'
else
print '--Failed'
--test all all spare game, strike on last ball
delete from scores
exec dbo.usp_ScoreBowlingGame 1, 1, 9, 0
exec dbo.usp_ScoreBowlingGame 1, 1, 9, 0
exec dbo.usp_ScoreBowlingGame 1, 1, 9, 0
exec dbo.usp_ScoreBowlingGame 1, 1, 9, 0
exec dbo.usp_ScoreBowlingGame 1, 1, 9, 0
exec dbo.usp_ScoreBowlingGame 1, 1, 9, 0
exec dbo.usp_ScoreBowlingGame 1, 1, 9, 0
exec dbo.usp_ScoreBowlingGame 1, 1, 9, 0
exec dbo.usp_ScoreBowlingGame 1, 1, 9, 0
exec dbo.usp_ScoreBowlingGame 1, 1, 9, 10
select @Score = max(numberofpins) from scores with (nolock) where userid = 1
print 'All spare game, strike on last ball'
if @Score = 119
print '--Passed'
else
print '--Failed'
delete from scores
exec dbo.usp_ScoreBowlingGame 1, 1, 9, 0
exec dbo.usp_ScoreBowlingGame 1, 1, 9, 0
exec dbo.usp_ScoreBowlingGame 1, 1, 9, 0
exec dbo.usp_ScoreBowlingGame 1, 1, 9, 0
exec dbo.usp_ScoreBowlingGame 1, 1, 9, 0
exec dbo.usp_ScoreBowlingGame 1, 1, 9, 0
exec dbo.usp_ScoreBowlingGame 1, 1, 9, 0
exec dbo.usp_ScoreBowlingGame 1, 1, 9, 0
exec dbo.usp_ScoreBowlingGame 1, 1, 9, 0
exec dbo.usp_ScoreBowlingGame 1, 10, 10, 10
select @Score = max(numberofpins) from scores with (nolock) where userid = 1
print '9 spares, all strikes in last frame'
if @Score = 138
print '--Passed'
else
print '--Failed'
delete from scores
exec dbo.usp_ScoreBowlingGame 1, 0, 0, 0
exec dbo.usp_ScoreBowlingGame 1, 0, 0, 0
exec dbo.usp_ScoreBowlingGame 1, 0, 0, 0
exec dbo.usp_ScoreBowlingGame 1, 0, 0, 0
exec dbo.usp_ScoreBowlingGame 1, 0, 0, 0
exec dbo.usp_ScoreBowlingGame 1, 0, 0, 0
exec dbo.usp_ScoreBowlingGame 1, 0, 0, 0
exec dbo.usp_ScoreBowlingGame 1, 0, 0, 0
exec dbo.usp_ScoreBowlingGame 1, 0, 0, 0
exec dbo.usp_ScoreBowlingGame 1, 0, 0, 0
select @Score = isnull(max(numberofpins), 0) from scores with (nolock) where userid = 1
print 'Total loss!'
if @Score = 0
print '--Passed'
else
print '--Failed'
All tests passed. The only real negative I can see is that the value for the extra ball in the tenth frame is not persisted. The final score returned is correct, but without the stored value you wouldn't be able to rescore the game if needed. This is just housekeeping, easy enough to add a third column to the table even though it would only get used on the tenth frame.
I'm please to announce that the team of Jon Winer, Matt Owen, and Namho Jung will each receive a SSC Polo Shirt for their solution. As Im mentioned earlier, I'll be posting another solution as soon as I find one that is interesting AND passes all the tests. To all of you who took time to participate, I hope you found it as much fun as my team at work did - and don't give up hope, you might yet win!