The Bowling Game Kata in Haskell

By
Posted on
Tags: haskell, kata, bowling

On the WPLUG mailing list I came across a post about the formation of a Pittsburgh Coding Dojo. The idea is to get a bunch of hackers together and have them work on solving a challenge problem with the goal of sharpening their programming skills and learning from each other.

There was a trial meeting on 31 March that focused on The Bowling Game Kata. The challenge was essentially to write some code that scores a full (ten-frame) game of bowling. A game is represented by a series of “rolls,” each being the number of pins knocked down by a roll of the bowling ball. The scoring function must determine frame boundaries from the sequence of rolls and score all ten frames according to the rules of bowling, i.e., taking into account spares and strikes and the final frame.

The challenge sounded like a fun lunch-break problem, and so I whipped up the following solution in Haskell. (You might find it interesting to compare this solution to the Java-based solutions on the web.)

{-
   My solution to "The Bowling Game Kata"
   Tom Moertel <tom@moertel.com>
   2006-04-05

   See http://butunclebob.com/ArticleS.UncleBob.TheBowlingGameKata
-}

module Bowling (score) where

import Test.HUnit

-- | Compute the score for the list of rolls 'rs'

score rs = sc 0 1 rs

-- accumulate the score 's' and frame count 'f' while consuming a
-- list of rolls 'rs' one frame at a time

sc s 11 _  = s           -- frame 11 means all done; return score
sc s f rs  = case rs of  -- otherwise, consume the frame & recurse
    10:rs'                -> sc' 3 rs'  -- strike
    x:y:rs' | x + y == 10 -> sc' 3 rs'  -- spare
            | otherwise   -> sc' 2 rs'  -- normal
    _                     -> error "ill-formed sequence of rolls"
  where
    -- accumulate the next 'n' rolls into the score and recurse
    sc' n rs' = sc (s + sum (take n rs)) (f + 1) rs'

Here are my unit tests:

{-
                      *** Unit tests ***

             *Bowling> runTestTT tests
             Cases: 9  Tried: 9  Errors: 0  Failures: 0
-}

tests = test
    [ "gutters"       ~: score  (rep 20  0)          ~?=   0
    , "ones"          ~: score  (rep 20  1)          ~?=  20
    , "fives"         ~: score  (rep 22  5)          ~?= 150
    , "strikes"       ~: score  (rep 12 10)          ~?= 300
    , "1 + gutters"   ~: score  (1 : rep 19 0)       ~?=   1
    , "first spare"   ~: score  (5:5:5 : rep 17 0)   ~?=  20
    , "first strike"  ~: score  (10:5:5 : rep 17 0)  ~?=  30
    , "last spare"    ~: rscore (5:5:5 : rep 18 0)   ~?=  15
    , "last strike"   ~: rscore (5:5:10 : rep 18 0)  ~?=  20
    ]
  where
    rep    = replicate
    rscore = score . reverse  -- reverse list and then score it

If you have a little free time, code up a solution in your favorite language.