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.