A few functions to score a bowling game
lein test
Our initial naive approach was to store the entire score card as the state. However, this quickly becomes complicated due the the application of bonus points (i.e. rolls count towards the current frame but must also be applied to previous frames where applicable). This had a state that looked like
; After 1 roll
{:frames [{:rolls [10] :bonus [] :pending 2}]
:rolls [10]}
; After 2 rolls
{:frames [{:rolls [10] :bonus [1] :pending 1}
{:rolls [1]}]
:rolls [10 1]}
After quickly scratching that, we decided to use a more functional approach of using a minimal state, and deriving everything that we could. Thus we stored just the rolls and the bonuses to be applied. Rather than in the above where bonus looks forward as a decrementing counter that is tied to the next roll, in the new approach, we simply put a place holder (i.e. the index of the roll that supplies the bonus). This means that rather than calculating bonus on each roll, we can do a single pass during scoring to inject the actual bonus values.
Thus the new state looks like:
; Initial
{:rolls []
:bonuses {}}
; After strike
{:rolls [10]
:bonuses {0 [1 2]}}
; After another roll of 7
{:rolls [10 7]
:bonuses {0 [1 2]}}
Our API is a classic reducer function of state -> roll -> next-state
. We use rolls as the action rather
than frames since it is a roll-centric game, that just happens to be scored by frames. i.e. it is unnatural
to hack in bonus rolls into frames.
We use rolls->frames
to generate frames from the rolls, which is necessary for calculating e.g.
- whether the game is complete
- whether to apply bonus for this roll (e.g. you would not have bonus for a strike after the 10th frame)
Finally for scoring, we initially had separate functions to calculate total and frame scores individually, but it is far nicer to generate score card as data, with the required elements just being looked up.
Going forward, we would like to use things like clojure.spec to firm down on the data at the boundaries of the system. There are currently some assumptions (e.g. not checking the total of a frame <= 10) which are intentionally omitted.