One of my favorite simple code katas with functional languages is to tally up bowling scores. Previously, I had done this with Erlang and Scala. Here is a Clojure version of the same including some tests:
(defn score-frame [rolls]
(let [[x1 x2 x3] rolls]
(cond
(nil? x1) nil
(nil? x2) nil
(nil? x3) (let [sumx (+ x1 x2)]
(if (< sumx 10) (list sumx))) ; open at end of game
(= x1 10) (cons (+ x1 x2 x3) (rest rolls)) ; strike
(= (+ x1 x2) 10) (cons (+ x1 x2 x3) (nthrest rolls 2)) ; spare
(< (+ x1 x2) 10) (cons (+ x1 x2) (nthrest rolls 2)) ; open
(true) nil
)
)
)
(defn roll-reduce [remaining-rolls]
(if (not (= nil remaining-rolls))
(let [result (score-frame remaining-rolls)]
(if (not (= nil result))
(cons (first result) (roll-reduce (rest result))))))
)
(defn score-ten-frames [rolls]
(take 10 (roll-reduce rolls)))
(defn tally-game [accum scores]
(if (> (count scores) 0) (let [new_accum (+ accum (first scores))] (cons new_accum (tally-game new_accum (rest scores)))))
)
(defn score-game [rolls]
(tally-game 0 (score-ten-frames rolls))
)
(assert (= '(30 60 90 120 150 180 210 240 270 300) (score-game '(10 10 10 10 10 10 10 10 10 10 10 10))))
(assert (= '(30 60 90 120 150 180 210 240 270 299) (score-game '(10 10 10 10 10 10 10 10 10 10 10 9))))
(assert (= '(30 60 90 120 150 180 210 240 270 290) (score-game '(10 10 10 10 10 10 10 10 10 10 10 0))))
(assert (= '(20 50 80 110 140 170 200 230 260 290) (score-game '(9 1 10 10 10 10 10 10 10 10 10 10 10))))
(assert (= '(30 60 90 120 150 180 210 240 269 289) (score-game '(10 10 10 10 10 10 10 10 10 10 9 1))))
(assert (= '(30 60 90 120 150 180 210 240 269 288) (score-game '(10 10 10 10 10 10 10 10 10 10 9 0))))
(assert (= '(30 60 90 120 150 180 210 240 260 280) (score-game '(10 10 10 10 10 10 10 10 10 10 0 10))))
(assert (= '(20 40 70 100 130 160 190 220 250 280) (score-game '(10 9 1 10 10 10 10 10 10 10 10 10 10))))
(assert (= '(30 60 90 120 150 180 210 239 259 279) (score-game '(10 10 10 10 10 10 10 10 10 9 1 10))))
(assert (= '(30 60 90 120 150 180 210 240 260 270) (score-game '(10 10 10 10 10 10 10 10 10 10 0 0))))
(assert (= '(20 40 60 80 100 120 140 160 180 200) (score-game '(10 9 1 10 9 1 10 9 1 10 9 1 10 9 1 10))))
(assert (= '(19 38 57 76 95 114 133 152 171 190) (score-game '(9 1 9 1 9 1 9 1 9 1 9 1 9 1 9 1 9 1 9 1 9))))
(assert (= '(19 38 57 76 95 114 133 152 171 180) (score-game '(9 1 9 1 9 1 9 1 9 1 9 1 9 1 9 1 9 1 9 0))))
(assert (= '(0 0 0 0 0 0 0 0 0 0) (score-ten-frames '(0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0))))
; Partial games
(assert (= nil (score-game nil)))
(assert (= nil (score-game '(0))))
(assert (= nil (score-game '(9))))
(assert (= nil (score-game '(10))))
(assert (= nil (score-game '(9 1))))
(assert (= nil (score-game '(10 10))))
(assert (= '(0) (score-game '(0 0))))
(assert (= '(1) (score-game '(0 1))))
(assert (= '(9) (score-game '(8 1))))
(assert (= '(9) (score-game '(9 0))))
(assert (= '(19) (score-game '(9 1 9))))
(assert (= '(19 28) (score-game '(10 9 0))))
(assert (= '(19 28) (score-game '(9 1 9 0))))
(assert (= '(20) (score-game '(10 9 1))))
(assert (= '(30) (score-game '(10 10 10))))
(assert (= '(30 59) (score-game '(10 10 10 9))))
(assert (= '(30 59 78 87) (score-game '(10 10 10 9 0))))
Note this code has been updated since initial posting to incorporate changes based on feedback in a comment from Mark Volkmann below. Mark has published an excellent overview of Clojure here, http://www.ociweb.com/jnb/jnbMar2009.html
Example:
C:\lang\clojure>java -jar clojure.jar bowling.clj
Clojure
user=> (score-game '(10 10 10 10 10 10 10 10 10 10 10 10))
(30 60 90 120 150 180 210 240 270 300)
user=> (score-game '(10 10 10 10 10 10 10 10 10 10 10 9))
(30 60 90 120 150 180 210 240 270 299)
user=> (score-game '(10 10 10 10 10 10 10 10 10 10 9 1))
(30 60 90 120 150 180 210 240 269 289)
user=> (score-game '(10 10 10 10 10 10 10 10 10 9 1 10))
(30 60 90 120 150 180 210 239 259 279)
user=> (score-game '(10 10 10 10 10 10 10 10 10 9 0))
(30 60 90 120 150 180 210 239 258 267)
user=> (score-game '(9 0 10 10 10 10 10 10 10 10 10 10 10))
(9 39 69 99 129 159 189 219 249 279)

3 comments:
I'm working on simplifying this. One thing you can do is replace the first function with the following. Note that Clojure convention is to make multi-word names all lowercase with words separated by hyphens.
(defn score-frame [rolls]
(let [[x1 x2 x3] rolls
sum2 (when (and x1 x2) (+ x1 x2))
sum3 (when (and sum2 x3) (+ sum2 x3))]
(cond
(nil? x1) nil
(nil? x2) nil
(nil? x3) (if (< sum2 10) (list sum2)) ; open at end of game
(= x1 10) (cons sum3 (score-frame (rest rolls))) ; strike
(= sum2 10) (cons sum3 (score-frame (nthrest rolls 2))) ; spare
(< sum2 10) (cons sum2 (score-frame (nthrest rolls 2))) ; open
true nil)))
I think this is a more idiomatic version of scoreFrame (sorry about the formatting, but Blogger won't let me use pre or code tags):
(defn scoreFrame [rolls]
(let [[x1 x2 x3] rolls]
(cond (or (nil? x1) (nil? x2)) nil
(nil? x3) (let [sumx (+ x1 x2)]
(if (< sumx 10) (list sumx))) ; open at end of game
(= x1 10) (cons (+ x1 x2 x3) (rest rolls)) ; strike
(= (+ x1 x2) 10) (cons (+ x1 x2 x3) (drop 2 rolls)) ; spare
(< (+ x1 x2) 10) (cons (+ x1 x2) (drop 3 rolls)) ; open
true nil)))
Some tips:
Clojure let does destructuring - no need to access members of a sequence individually in cases like this.
There's probably no need to bind a symbol to each of the segments of rolls that you want - just access them with drop.
The true at the end of the cond clauses doesn't need to be bracketed. In fact, I don't know why it isn't throwing an exception (trying to call a boolean as a function).
Traditional Lisp style is to put all the closing parens on the same line. I know that seems very odd coming from other languages, but it's the way everyone in the Lisp world does things.
I hope you're enjoying Clojure!
hye, i browsed for a bowling score coding today.
and found ur blog.
can i just ask how do we do the coding to display the output neatly?
&& do we need to prompt the user to enter the score?
Post a Comment