I was first introduced to the "Bowling Game" kata/exercise in "Agile Principles, Patterns, and Practices in C#" (by Robert and Micah Martin), which I read more than 10 years ago. Chapter 6 of that book is a 40+ page programming episode presented in dialog between two programmers using TDD to implement an algorithm for scoring a bowling game. Robert Martin has used it to teach TDD ever since. I was drawn to the exercise as a way to begin learning new programming languages or new approaches to testing. Here are a few previous renditions:
[[code]
bowling_test.go](https://github.com/mdw-go/testing/blob/main/bowling/bowling_test.go)- This rendition illustrates how to use my own Go testing module.
[[video] The "Bowling Game" Kata, in TCR (test && commit || revert)](https://www.youtube.com/watch?v=qNwvadGPF4k)
- In this rendition I was practicing a new technique: TCR
[[video] The "Bowling Game" Kata in TCR using "Functional Fixtures"](https://www.youtube.com/watch?v=hXiWx50RNKE)
- In this rendition I present a new approach to standard-library Go testing using "functional options".
[[code]
bowling.rkt](https://gist.github.com/mdwhatcott/7f686303be9e9e1dbe42891814e15bb2)- A few months ago I dabbled in Racket, a functional programming language descended from Scheme.
I'm currently learning Clojure (a functional language descended from Lisp that runs on the JVM) so I took yet another stab at the "Bowling Game" today:
| (ns bowling.core-spec | |
| (:require [speclj.core :refer :all])) | |
| (def all-pins 10) | |
| (def frames-per-game 10) | |
| (defn game-over [frame] (= frame frames-per-game)) | |
| (defn strike-score [rolls] (+ all-pins (nth rolls 1) (nth rolls 2))) | |
| (defn spare-score [rolls] (+ all-pins (nth rolls 2))) | |
| (defn frame-score [rolls] (+ (first rolls) (second rolls))) | |
| (defn is-strike [rolls] (= all-pins (first rolls))) | |
| (defn is-spare [rolls] (= all-pins (frame-score rolls))) | |
| (defn bowl [rolls] | |
| (loop [rolls rolls | |
| frame 0 | |
| score 0] | |
| (if (game-over frame) | |
| score | |
| (cond | |
| (is-strike rolls) | |
| (recur (rest rolls) | |
| (inc frame) | |
| (+ score (strike-score rolls))) | |
| (is-spare rolls) | |
| (recur (rest (rest rolls)) | |
| (inc frame) | |
| (+ score (spare-score rolls))) | |
| :else | |
| (recur (rest (rest rolls)) | |
| (inc frame) | |
| (+ score (frame-score rolls))))))) | |
| ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |
| (defn roll-many [times pins] | |
| (take times (cycle [pins]))) | |
| (describe "gutter game" | |
| (it "scores zero points" | |
| (let [gutter-game (roll-many 20 0)] | |
| (should= 0 (bowl gutter-game))))) | |
| (describe "all ones" | |
| (it "scores one point per throw" | |
| (let [all-ones (roll-many 20 1)] | |
| (should= 20 (bowl all-ones))))) | |
| (describe "spare" | |
| (it "counts the next roll as a bonus" | |
| (let [game (concat [4 6 3 1] (roll-many 16 0))] | |
| (should= (+ 4 6 3 3 1) | |
| (bowl game))))) | |
| (describe "strike" | |
| (it "counts the next two rolls as a bonus" | |
| (let [game (concat [10 3 4 1] (roll-many 15 0))] | |
| (should= (+ 10 3 4 3 4 1) | |
| (bowl game))))) | |
| (describe "perfection" | |
| (it "should score 300" | |
| (let [game (roll-many 12 10)] | |
| (should= 300 (bowl game))))) |