Skip to content

Instantly share code, notes, and snippets.

@gfixler
Last active August 29, 2015 14:22
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gfixler/18e16becbd4fb54e517b to your computer and use it in GitHub Desktop.
Save gfixler/18e16becbd4fb54e517b to your computer and use it in GitHub Desktop.
Uncle Bob's kata, sans OO, TDD, kata
Here's a purely-functional take on collecting rolls (no scoring yet)
First a data type to represent the 4 states a frame can be in:
> data Frame = Roll Int | Open Int Int | Spare Int | Strike deriving (Show)
This just simplifies bounds-checks on input roll values:
> badnum :: Int -> Bool
> badnum n = n < 0 || n > 10
`roll` adds a new roll value to a list of Frames, returning the updated list:
> roll :: Int -> [Frame] -> [Frame]
> roll n (Roll r : rs) | badnum (n+r) = undefined -- illegal
> | n + r == 10 = Spare r : rs -- spare
> | otherwise = Open r n : rs -- open frame
> roll 10 rs = Strike : rs -- strike
> roll n rs | badnum n = undefined -- illegal
> | otherwise = Roll n : rs -- first roll
Cases work from the top to the bottom - we have 3; first we check if
you're mid-frame by pattern-matching on a list with a `Roll r` at its
head. Failing that, we know you're not mid-frame, so we check for an
incoming roll of `10`, indicating a strike. Failing that, you must be
starting a frame, so we handle a first roll. Cases usually fall through
from specific to general like this.
In the first case - mid-frame - we first do a bounds-check on the sum
of the two rolls (and error out if needed - not the best strategy). If
that passes, we check for the spare, and, failing that, we go with the
open frame. In the spare and open-frame cases, we replace the `Roll` at
the list head, so a `Roll 7` may become an `Open 7 2`, or a `Spare`.
Strikes must come second, because if we're in a state to get a strike,
we aren't mid-frame, so we do the handling of mid-frameness up front.
If not mid-frame, we're at the start of a new frame, so we can first
check for the strike, and if we didn't get it, we fall through to the
only state left (new frame), and tack on a `Roll n` to the `Roll`s list.
Something interesting is that, by tacking on rolls to the front of the
`Roll` list, we build up the game in reverse. This means that when we
score the game later, we don't need to look ahead to score spares and
strikes; we can just keep passing the last two frames through in a
recursion, and if we're on a spare, add in the first, or if we're on a
strike, add in both.
Another interesting aspect is that we can fold scores, so...
foldl (flip roll) [] [2,4,3,6,5,5,8,0,10,6,3,10,7,3]
...yields...
[Spare,Strike,Open 6 3,Strike,Open 8 0,Spare,Open 3 6,Open 2 4]
In fact, just by changing `foldl` to `scanl` (and tacking on a `mapM_ print`
to print each iteration on its own line), we see the intermediate steps,
so we can watch the game state unfold as it consumes rolls:
mapM_ print $ scanl (flip roll) [] [2,4,3,6,5,5,8,0,10,6,3,10,10,7,3]
[]
[Roll 2]
[Open 2 4]
[Roll 3,Open 2 4]
[Open 3 6,Open 2 4]
[Roll 5,Open 3 6,Open 2 4]
[Spare,Open 3 6,Open 2 4]
[Roll 8,Spare,Open 3 6,Open 2 4]
[Open 8 0,Spare,Open 3 6,Open 2 4]
[Strike,Open 8 0,Spare,Open 3 6,Open 2 4]
[Roll 6,Strike,Open 8 0,Spare,Open 3 6,Open 2 4]
[Open 6 3,Strike,Open 8 0,Spare,Open 3 6,Open 2 4]
[Strike,Open 6 3,Strike,Open 8 0,Spare,Open 3 6,Open 2 4]
[Strike,Strike,Open 6 3,Strike,Open 8 0,Spare,Open 3 6,Open 2 4]
[Roll 7,Strike,Strike,Open 6 3,Strike,Open 8 0,Spare,Open 3 6,Open 2 4]
[Spare,Strike,Strike,Open 6 3,Strike,Open 8 0,Spare,Open 3 6,Open 2 4]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment