Bowling Scores
Sunday, August 30, 2015
Today we are going to explore building a bowling score calculator using Factor. In particular, we will be scoring ten-pin bowling.
There are a lot of ways to “golf” this, including this short version in F#, but we will build this in several steps through transformations of the input. The test input is a string representation of the hits, misses, spares, and strikes. The output will be a number which is your total score. We will assume valid inputs and not do much error-checking.
A sample game might look like this:
12X4--3-69/-98/8-8-
Our first transformation is to convert each character to a number of
pins that have been knocked down for each ball. Strikes are denoted with
X
, spares with /
, misses with -
, and normal hits with a number.
: pin ( last ch -- pin )
{
{ CHAR: X [ 10 ] }
{ CHAR: / [ 10 over - ] }
{ CHAR: - [ 0 ] }
[ CHAR: 0 - ]
} case nip ;
We use this to convert the entire string into a series of pins knocked down for each ball.
: pins ( str -- pins )
f swap [ pin dup ] { } map-as nip ;
A single frame will be either one ball, if a strike, or two balls. We are going to use cut-slice instead of cut because it will be helpful later.
: frame ( pins -- rest frame )
dup first 10 = 1 2 ? short cut-slice swap ;
A game is 9 “normal” frames and then a last frame that could have up to three balls in it.
: frames ( pins -- frames )
9 [ frame ] replicate swap suffix ;
Some frames will trigger a bonus. Strikes add the value of the next two balls. Spares add the value of the next ball. We build this by “un-slicing” the frame and calling sum on the next balls.
: bonus ( frame -- bonus )
[ seq>> ] [ to>> tail ] [ length 3 swap - ] tri head sum ;
We can score the frames by checking for frames where all ten pins are knocked down (either spares or strikes) and adding their bonus.
: scores ( frames -- scores )
[ [ sum ] keep over 10 = [ bonus + ] [ drop ] if ] map ;
We can solve the original goal by just adding all the scores:
: bowl ( str -- score )
pins frames scores sum ;
And write a bunch of unit tests to make sure it works:
{ 0 } [ "---------------------" bowl ] unit-test
{ 11 } [ "------------------X1-" bowl ] unit-test
{ 12 } [ "----------------X1-" bowl ] unit-test
{ 15 } [ "------------------5/5" bowl ] unit-test
{ 20 } [ "11111111111111111111" bowl ] unit-test
{ 20 } [ "5/5-----------------" bowl ] unit-test
{ 20 } [ "------------------5/X" bowl ] unit-test
{ 40 } [ "X5/5----------------" bowl ] unit-test
{ 80 } [ "-8-7714215X6172183-" bowl ] unit-test
{ 83 } [ "12X4--3-69/-98/8-8-" bowl ] unit-test
{ 150 } [ "5/5/5/5/5/5/5/5/5/5/5" bowl ] unit-test
{ 144 } [ "XXX6-3/819-44X6-" bowl ] unit-test
{ 266 } [ "XXXXXXXXX81-" bowl ] unit-test
{ 271 } [ "XXXXXXXXX9/2" bowl ] unit-test
{ 279 } [ "XXXXXXXXXX33" bowl ] unit-test
{ 295 } [ "XXXXXXXXXXX5" bowl ] unit-test
{ 300 } [ "XXXXXXXXXXXX" bowl ] unit-test
{ 100 } [ "-/-/-/-/-/-/-/-/-/-/-" bowl ] unit-test
{ 190 } [ "9/9/9/9/9/9/9/9/9/9/9" bowl ] unit-test
This is available on my GitHub.