Last active
August 29, 2015 14:22
-
-
Save gfixler/18e16becbd4fb54e517b to your computer and use it in GitHub Desktop.
Uncle Bob's kata, sans OO, TDD, kata
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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