Clojure Kata #2 – The Bowling Game
As mentioned in the previous blog post, I?ve been learning Clojure and I decided to do so while practicing a couple of coding katas. The second kata that I want to walk through is the bowling game.
First, we needed to decide which testing framework to use. Our choice fell on Midje, whose syntax looked very compelling to me on first glance. So we added the following line to .lein/profiles.clj :
{:user {:plugins [[lein-midje "3.0.0"]]}}
Initializing a new project is as simple as issuing the following command:
$ lein new midje bowling_game
We?re now ready for our first test.
Test #1: Gutter game
Here?s the code for our first test.
(facts "When calculating the score of a bowling game" (fact "it returns a zero score for a complete gutter game" (score(repeat 20 0)) => 0))
Running this test without having the score function implemented obviously results in an error. In order to satisfy this test, we have to implement this function.
(defn score [rolls] 0)
We?re putting out the least amount of code in order to satisfy this test. So just returning zero will do just fine for now.
Test #2: A game of two pin frames
For our next test, we create a complete game of only two pin frames.
(fact "it returns a score of twenty for a game of two pin frames"
(score(repeat 20 1)) => 20))
In order to make this test pass, we have to return a score that is the sum of all the pins knocked down for each roll.
(defn score [rolls] (reduce + rolls))
The tests all pass, including the test for the gutter game and we still don?t have anything to refactor. Let?s move on to the next test case.
Test #3: A game with a spare
The next test is going to create a game that includes a spare.
(fact "it returns a score that includes the next roll for a spare"
(score(concat (5 5 3) (repeat 17 0))) => 16))
The score of a spare is calculated by taking the 10 pins from the frame and adding the number of pins of the first roll of the next frame.
At this point we felt the need to actually divide up the scores and bring in the notion of a frame and a game. So we commented out our new test and went on a refactoring spree. This is what we came up with:
(defn make-frames[rolls] (lazy-seq (cons (take 2 rolls) (make-frames (drop 2 rolls))))) (defn make-game[rolls] (take 10 (make-frames rolls))) (defn calculate-frame-score [frame_rolls] (reduce + frame_rolls)) (defn calculate-game-score [frame_scores] (reduce + frame_scores)) (defn score [rolls] (calculate-game-score (map calculate-frame-score (make-game rolls))))
As you can see, we created separate functions for creating a game (make-game) that consists of 10 frames (make-frames). Therefore we also added separate functions for calculating a frame score (calculate-frame-score) and ultimately the total score of a game (calculate-game-score).
All previous tests still pass, so we?re able to uncomment the third test that tests the scenario of a game with a spare.
(defn spare? [rolls] (= 10 (apply + (take 2 rolls)))) (defn number-of-roles-for-frame-score [rolls] (cond (spare? rolls) 3 :else 2)) (defn make-frames[rolls] (lazy-seq (cons (take (number-of-roles-for-frame-score rolls) rolls) (make-frames (drop 2 rolls))))) (defn make-game[rolls] (take 10 (make-frames rolls))) (defn calculate-frame-score [frame_rolls] (reduce + frame_rolls)) (defn calculate-game-score [frame_scores] (reduce + frame_scores)) (defn score [rolls] (calculate-game-score (map calculate-frame-score (make-game rolls))))
Implementing this scenario was quite easy after our refactoring. We added a function that checks for a spare? frame. We also introduced an additional function that uses the new spare? function for determining how many rolls need to be incorporated for the score of the frame. Now we simply need to replace ?(take 2 rolls)? with ?(take (number-of-roles-for-frame-score rolls) rolls)? in the make-frames function.
All tests are green now. Before moving on to the next test case, we also performed a small refactoring to the test code as well.
(fact "it returns a score that includes the next roll for a spare frame" (score(concat '(5 5) '(3 0) (repeat 16 0))) => 16)
This better communicates the notion of two frames, the first with a spare and the second being a regular frame.
Test #4: A game with a strike
The next one is going to set up the scenario of a game with a strike.
(fact "it returns a score that includes the next two rolls for a strike frame" (score(concat '(10) '(5 3) (repeat 16 0))) => 26)
The score of a strike is calculated by taking the 10 pins from the frame and adding the number of pins of the next two rolls.
(defn spare? [rolls] (= 10 (apply + (take 2 rolls)))) (defn strike? [rolls] (= 10 (first rolls))) (defn number-of-roles-for-frame-score [rolls] (cond (strike? rolls) 3 (spare? rolls) 3 :else 2)) (defn number-of-rolls-for-frame [rolls] (if (strike? rolls) 1 2)) (defn make-frames[rolls] (lazy-seq (cons (take (number-of-roles-for-frame-score rolls) rolls) (make-frames (drop (number-of-rolls-for-frame rolls) rolls))))) (defn make-game[rolls] (take 10 (make-frames rolls))) (defn calculate-frame-score [frame_rolls] (reduce + frame_rolls)) (defn calculate-game-score [frame_scores] (reduce + frame_scores)) (defn score [rolls] (calculate-game-score (map calculate-frame-score (make-game rolls))))
We added a function that checks for a strike?, which we added to the number-of-roles-for-frame-score function just as we did for the spare? function. Besides this, we also needed to add a function named number-of-roles-for-frame that determines how many rolls to skip when recursively calling make-frames.
All tests are green again, and our story has come to an end.
The full code
We slightly refactored our tests by introducing a function named zero-frames that makes it easier to fill up a game with frames of zero pins. Here?s the complete code of our tests.
(defn zero-frames [number] (repeat (* 2 number) 0)) (facts "When calculating the score of a bowling game" (fact "it returns a zero score for a complete gutter game" (score(zero-frames 10)) => 0) (fact "it returns a score of twenty for a game of only two pin frames" (score(repeat 20 1)) => 20) (fact "it returns a score that includes the next roll for a spare frame" (score(concat '(5 5) '(3 0) (zero-frames 8))) => 16) (fact "it returns a score that includes the next two rolls for a strike frame" (score(concat '(10) '(5 3) (zero-frames 8))) => 26) (fact "it returns a score of three hundred the perfect game" (score(concat (repeat 12 10))) => 300))
And here?s the complete code of our implementation.
(defn spare? [rolls] (= 10 (apply + (take 2 rolls)))) (defn strike? [rolls] (= 10 (first rolls))) (defn number-of-roles-for-frame-score [rolls] (cond (strike? rolls) 3 (spare? rolls) 3 :else 2)) (defn number-of-rolls-for-frame [rolls] (if (strike? rolls) 1 2)) (defn make-frames[rolls] (lazy-seq (cons (take (number-of-roles-for-frame-score rolls) rolls) (make-frames (drop (number-of-rolls-for-frame rolls) rolls))))) (defn make-game[rolls] (take 10 (make-frames rolls))) (defn calculate-frame-score [frame_rolls] (reduce + frame_rolls)) (defn calculate-game-score [frame_scores] (reduce + frame_scores)) (defn score [rolls] (calculate-game-score (map calculate-frame-score (make-game rolls))))
While there?s probably a better way to implement this, I really learned from following the TDD flow. I?ve used Sublime Text as my editor along with the SublimeREPL package. I found being able to run tests and execute functions directly from the REPL to be very powerful with Clojure. I probably just scratched the surface.
Until next time.
Recent Comments