Bowling Kata


Our team’s been doing Katas to get the hang of TDD. One such kata (calculating bowling scores) struck me as insanely simple with pattern matching in F#:

let rec score acc = function
    | 10 :: (a :: b :: _ as t) -> score (acc + 10 + a + b) t // strike
    | 10 :: t -> score acc t // final frames following strike 
    | a :: b :: c :: [] when a + b = 10 -> acc + 10 + c // final frame spare
    | a :: b :: (c :: _ as t) when a + b = 10 -> score (acc + 10 + c) t // spare 
    | h :: t -> score (acc + h) t // normal roll
    | [] –> acc

// Tests

let
test game expected =
    let result = score 0 game
    if result <> expected then
        printfn “%A = %i (expected %i)” game result expected
 
test [0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0] 0 // gutter game
test [1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1] 20 // all ones
test [5;5;3] 16 // one spare (wrong!)
test [5;5; 3;0; 0;0; 0;0; 0;0; 0;0; 0;0; 0;0; 0;0; 0;0] 16 // one spare 
test [0;0; 0;0; 0;0; 0;0; 0;0; 0;0; 0;0; 0;0; 0;0; 5;5;3] 13 // one spare

test [10;3;4;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0] 24
// one strike
test [10;10;10;10;10;10;10;10;10;10;10;10] 300 // perfect game
test [1;4; 4;5; 6;4; 5;5; 10; 0;1; 7;3; 6;4; 10; 2;8;6] 133 

Oops!

Thanks to Brian Friesen (below in the comments) for pointing out a bug. This wasn’t correctly handling spares in the final frame! One line fix (highlighted).

It brings up another issue in one of the test cases as well. [5;5;3] should produce a different score depending on whether it’s at the start or end of a complete game. Added two cases for that.

Comments (7)

  1. Joel says:

    I'm pretty sure that both cases of "_ as t" can be replaced with simply "t".

  2. @Joel: I don't believe so. The 't' is naming the whole parenthesized expression, not just the underscore. For example:

    let a :: (b :: c :: _ as t) = [1; 2; 3; 4]

    val t : int list = [2; 3; 4]

    val c : int = 3

    val b : int = 2

    val a : int = 1

    Notice that 't' is the whole original tail while 'b', and 'c' allow "peeking" ahead. I think this is the very thing that makes this solution so simple.

  3. Scott Seely says:

    The one strike line should total 17. Strike equals the 10 + the next two balls. In your case, this is 17.

  4. Scott Seely says:

    Crap- nevermind/disregard. Did the math again, gotta add the 3 +4 again.

  5. Joel says:

    Ah, I see what you mean. Very clever.

  6. Brian Friesen says:

    It doesn't seem to handle the final frame correctly.

    test [1;4; 4;5; 6;4; 5;5; 10; 0;1; 7;3; 6;4; 10; 2;8;6] 133

    results in:

    [1; 4; 4; 5; 6; 4; 5; 5; 10; 0; 1; 7; 3; 6; 4; 10; 2; 8; 6] = 139 (expected 133)

    Example taken from Uncle Bob:

    butunclebob.com/…/Bowling%20Game%20Kata.ppt

  7. @Brian You're right! I just fixed it and added an "Oops!" section above. Thanks for catching!