-- Working on Ron Jeffries' bowling kata:
--   http://www.xprogramming.com/xpmag/acsBowling.htm
--   http://www.xprogramming.com/xpmag/dbcHaskellBowling.htm
--   http://www.google.com/search?q=+site:www.xprogramming.com+jeffries+bowling
-- This is a new version of Haskell bowling, written from scratch.
import Test.HUnit

-- Pins knocked down by each ball.
type Score = Int

-- Number of points scored.
type Balls = [Int]

-- Given the number of pins knocked down
-- by each ball, score the game.
scoreGame :: Balls -> Score
scoreGame balls =
  sum (take 10 (reduce scoreFrame balls))

-- A slightly messier cousin of 'foldr'.
reduce :: ([a] -> (b,[a])) -> [a] -> [b]
reduce f ys = x:reduce f ys'
  where (x,ys') = f ys

-- Score one frame of a bowling game,
-- and calculate the starting point for
-- the next frame.
scoreFrame :: Balls -> (Score, Balls)
scoreFrame (x1:    y1:y2:ys) | x1 == 10 =
  (x1+y1+y2, y1:y2:ys)  -- Strike
scoreFrame (x1:x2: y1:ys) | x1+x2 == 10 =
  (x1+x2+y1, y1:ys)     -- Spare
scoreFrame (x1:x2: ys) =
  (x1+x2,    ys)        -- Open frame

-- Lists of balls, and the desired scores.
testData :: [(Balls,Score)]
testData = [
  (10:  perfect 11, 300), -- Strike
  ( 9:1:perfect 11, 290), -- Spare
  ( 8:1:perfect 11, 279), -- Open frame
  ( 8:1: 9:1:perfect 10,  269),
  ( 7:2: 6:3:perfect 10,  258),
  (perfect 9++[10,10, 0], 290),
  (perfect 9++[10, 5, 5], 285),
  (perfect 9++[10, 0,10], 280),
  (perfect 9++[10, 0, 0], 270),
  (perfect 9++[ 9, 0],    267),
  (perfect 9++[ 9, 1, 5], 274),
  -- Two from http://www.xprogramming.com/xpmag/dbcHaskellBowling.htm
  ([10,5,5, 10,5,5, 10,5,5, 10,5,5, 10,5,5, 10], 200),
  ([5,5, 10,5,5, 10,5,5, 10,5,5, 10,5,5, 10,5,5], 200),
  -- Two from http://www.xprogramming.com/xpmag/dbcRecurringDrama.htm
  ([0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 10,2,3], 15),
  ([0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 10,  2,3], 20),
  (gutter 20, 0)]         -- Missed all

-- A list of 'n' perfect balls.
perfect n = replicate n 10

-- A list of 'n' gutter balls.
gutter n = replicate n 0

-- Construct a unit test asserting that
-- we calculate the expected score.
testFromData (balls, score) =
  ("Scoring " ++ show balls) ~:
    score ~=? scoreGame balls

-- Build a list of tests and run it.
tests = test (map testFromData testData)
main = runTestTT tests

{-
$ runhaskell Bowling.hs 
Cases: 12  Tried: 12  Errors: 0  Failures: 0
Counts {cases = 12, tried = 12, errors = 0, failures = 0}
-}
