Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

integrated score function #441

Merged
merged 2 commits into from
Nov 24, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 15 additions & 8 deletions exercises/bowling/HINTS.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
## Hints

To complete this exercise, you need to create the data type `Bowling`
and implement the following functions:
To complete this exercise you need to implement the function `score`,
that takes a sequence of bowling *rolls* and returns the final score or
the appropriate error:

- `bowlingStart` returns an empty `Bowling`.
- `roll` evaluates an input roll, returning the new `Bowling`.
- `score` calculates the total score at the end if it is regular game.
```haskell
score :: [Int] -> Either BowlingError Int

You will find a dummy data declaration and type signatures already in place,
but it is up to you to define the functions and create a meaningful data type,
newtype or type synonym.
data BowlingError = IncompleteGame
| InvalidRoll { rollIndex :: Int, rollValue :: Int }
deriving (Eq, Show)
```

You will find these definitions already in place, but it is up to you
to define the function.

Keep in mind that the test suite expects the rolls to be numbered
starting from zero.
86 changes: 45 additions & 41 deletions exercises/bowling/examples/success-standard/src/Bowling.hs
Original file line number Diff line number Diff line change
@@ -1,50 +1,54 @@
module Bowling (bowlingStart, roll, score) where
module Bowling (score, BowlingError(..)) where

import Data.List (tails)

data Bowling = BowlingScore [Frame] |
BowlingFailure
data BowlingError = IncompleteGame
| InvalidRoll { rollIndex :: Int, rollValue :: Int }
deriving (Eq, Show)

type Roll = Int

data Frame = Frame { rolls :: [Roll], finalFrame :: Bool }
data Frame = Frame { whichFrame :: Int, rolls :: [Roll] }

bowlingStart :: Bowling
bowlingStart = BowlingScore [Frame [] False]
type ScoreState = Either BowlingError (Frame, Int, Int) -- (frame, rollIndex, totalScore)

roll :: Bowling -> Int -> Bowling
roll BowlingFailure _ = BowlingFailure
roll _ r | r < 0 || r > 10 = BowlingFailure
roll (BowlingScore (f@(Frame rs True):fs)) r
| length rs < 2 = BowlingScore $ addRoll f r:fs
| throws f == 2 && (head rs == 10 || spare f) = addFillBall
| otherwise = BowlingFailure -- no more rolls possible
score :: [Int] -> Either BowlingError Int
score rolls' = totalScoreState >>= completedScore
where
addFillBall :: Bowling
totalScoreState = foldl scoreRoll startState rolls''
startState = Right (Frame 1 [], 0, 0)
rolls'' = filter (not . null) $ tails rolls'
completedScore :: (Frame, Int, Int) -> Either BowlingError Int
completedScore (f, _, totalScore)
| complete f && finalFrame f = Right totalScore
| otherwise = Left IncompleteGame

scoreRoll :: ScoreState -> [Roll] -> ScoreState
scoreRoll scoreState rolls' = do
(f, ri, s) <- scoreState
scoreRoll' f ri s rolls'

scoreRoll' :: Frame -> Int -> Int -> [Roll] -> ScoreState
scoreRoll' _ ri _ (r:_) | r < 0 || r > 10 = Left $ InvalidRoll ri r
scoreRoll' f@(Frame 10 (fr:_)) ri totalScore (r:_)
| throws f < 2 = Right (addRoll f r, succ ri, totalScore + fr + r)
| throws f == 2 && (fr == 10 || spare f) = addFillBall
| otherwise = Left $ InvalidRoll ri r -- no more rolls possible
where
addFillBall :: ScoreState
addFillBall
| firstRoll == 10 && secondRoll /= 10 && secondRoll + r > 10 = BowlingFailure
| otherwise = BowlingScore $ addRoll f r:fs
| firstRoll == 10 && secondRoll /= 10 && secondRoll + r > 10 = Left $ InvalidRoll ri r
| otherwise = Right (addRoll f r, succ ri, totalScore + r)
where [firstRoll, secondRoll] = rolls f
roll (BowlingScore (f@(Frame _ False):fs)) r
| complete f = BowlingScore $ Frame [r] (length fs == 8):f:fs
| pins f + r <= 10 = BowlingScore $ addRoll f r:fs
roll _ _ = BowlingFailure

score :: Bowling -> Maybe Int
score BowlingFailure = Nothing
score (BowlingScore fs)
| length fs < 10 || any (not . complete) fs = Nothing -- incomplete game
| otherwise = Just $ foldl addScore 0 fsTails
where
fsTails = take 10 . tails . reverse $ fs

addScore :: Int -> [Frame] -> Int
addScore acc (f:fts) = acc + fscore f fts
addScore acc [] = acc
scoreRoll' f@(Frame n _) ri totalScore rs@(r:_)
| complete f = Right (Frame (succ n) [r], succ ri, totalScore + fscore f rs)
| pins f + r <= 10 = Right (addRoll f r, succ ri, totalScore)
scoreRoll' _ ri _ (r:_) = Left $ InvalidRoll ri r
scoreRoll' _ _ _ _ = undefined -- to avoid compiler warning

-- Frame functions
addRoll :: Frame -> Roll -> Frame
addRoll f r = Frame (rolls f ++ [r]) (finalFrame f)
addRoll f r = Frame (whichFrame f) (rolls f ++ [r])

throws :: Frame -> Int
throws = length . rolls
Expand All @@ -58,23 +62,23 @@ strike f = throws f == 1 && pins f == 10
spare :: Frame -> Bool
spare f = throws f == 2 && pins f == 10

finalFrame :: Frame -> Bool
finalFrame (Frame n _) = n == 10

complete :: Frame -> Bool
complete f
| not $ finalFrame f = strike f || throws f == 2
| head (rolls f) == 10 || sum (take 2 $ rolls f) == 10 = throws f == 3
| otherwise = throws f == 2

fscore :: Frame -> [Frame] -> Int
fscore f nextFrames = pins f + bonus
fscore :: Frame -> [Roll] -> Int
fscore f rs = pins f + bonus
where
bonus
| finalFrame f = 0
| strike f = nextRoll + nextToNextRoll
| spare f = nextRoll
| otherwise = 0
nextRoll = head nextFrameRolls
nextToNextRoll
| length nextFrameRolls > 1 = head $ tail nextFrameRolls
| otherwise = head nextToNextFrameRolls
nextFrameRolls = rolls $ head nextFrames
nextToNextFrameRolls = rolls . head . tail $ nextFrames
nextRoll = head rs
nextToNextRoll = head $ tail rs

14 changes: 5 additions & 9 deletions exercises/bowling/src/Bowling.hs
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
module Bowling (bowlingStart, roll, score) where
module Bowling (score, BowlingError(..)) where

data Bowling = Dummy
data BowlingError = IncompleteGame
| InvalidRoll { rollIndex :: Int, rollValue :: Int }
deriving (Eq, Show)

bowlingStart :: Bowling
bowlingStart = undefined

roll :: Bowling -> Int -> Bowling
roll = undefined

score :: Bowling -> Maybe Int
score :: [Int] -> Either BowlingError Int
score = undefined

65 changes: 33 additions & 32 deletions exercises/bowling/test/Tests.hs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Data.Foldable (for_)
import Test.Hspec (Spec, describe, it, shouldBe)
import Test.Hspec.Runner (configFastFail, defaultConfig, hspecWith)

import Bowling (bowlingStart, roll, score)
import Bowling (score, BowlingError(..))

main :: IO ()
main = hspecWith defaultConfig {configFastFail = True} specs
Expand All @@ -16,127 +16,128 @@ specs = describe "bowling" $

test Case{..} = it description assertion
where
assertion = result `shouldBe` expected
result = score $ foldl roll bowlingStart rolls
assertion = result `shouldBe` expected
result = score rolls

-- Test cases adapted from `exercism/x-common/bowling` on 2016-11-20.

data Case = Case { description :: String
, rolls :: [Int]
, expected :: Maybe Int
, expected :: Either BowlingError Int
}

cases :: [Case]
cases = [ Case { description = "should be able to score a game with all zeros"
, rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
, expected = Just 0
, expected = Right 0
}
, Case { description = "should be able to score a game with no strikes or spares"
, rolls = [3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6]
, expected = Just 90
, expected = Right 90
}
, Case { description = "a spare followed by zeros is worth ten points"
, rolls = [6, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
, expected = Just 10
, expected = Right 10
}
, Case { description = "points scored in the roll after a spare are counted twice"
, rolls = [6, 4, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
, expected = Just 16
, expected = Right 16
}
, Case { description = "consecutive spares each get a one roll bonus"
, rolls = [5, 5, 3, 7, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
, expected = Just 31
, expected = Right 31
}
, Case { description = "a spare in the last frame gets a one roll bonus that is counted once"
, rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 3, 7]
, expected = Just 17
, expected = Right 17
}
, Case { description = "a strike earns ten points in a frame with a single roll"
, rolls = [10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
, expected = Just 10
, expected = Right 10
}
, Case { description = "points scored in the two rolls after a strike are counted twice as a bonus"
, rolls = [10, 5, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
, expected = Just 26
, expected = Right 26
}
, Case { description = "consecutive strikes each get the two roll bonus"
, rolls = [10, 10, 10, 5, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
, expected = Just 81
, expected = Right 81
}
, Case { description = "a strike in the last frame gets a two roll bonus that is counted once"
, rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 7, 1]
, expected = Just 18
, expected = Right 18
}
, Case { description = "rolling a spare with the two roll bonus does not get a bonus roll"
, rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 7, 3]
, expected = Just 20
, expected = Right 20
}
, Case { description = "strikes with the two roll bonus do not get bonus rolls"
, rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 10, 10]
, expected = Just 30
, expected = Right 30
}
, Case { description = "a strike with the one roll bonus after a spare in the last frame does not get a bonus"
, rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 3, 10]
, expected = Just 20
, expected = Right 20
}
, Case { description = "all strikes is a perfect game"
, rolls = [10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10]
, expected = Just 300
, expected = Right 300
}
, Case { description = "rolls can not score negative points"
, rolls = [-1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
, expected = Nothing
, expected = Left $ InvalidRoll 0 (-1)
}
, Case { description = "a roll can not score more than 10 points"
, rolls = [11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
, expected = Nothing
, expected = Left $ InvalidRoll 0 11
}
, Case { description = "two rolls in a frame can not score more than 10 points"
, rolls = [5, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
, expected = Nothing
, expected = Left $ InvalidRoll 1 6
}
, Case { description = "bonus roll after a strike in the last frame can not score more than 10 points"
, rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 11]
, expected = Nothing
, expected = Left $ InvalidRoll 19 11
}
, Case { description = "two bonus rolls after a strike in the last frame can not score more than 10 points"
, rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 5, 6]
, expected = Nothing
, expected = Left $ InvalidRoll 20 6
}
, Case { description = "two bonus rolls after a strike in the last frame can score more than 10 points if one is a strike"
, rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 10, 6]
, expected = Just 26
, expected = Right 26
}
, Case { description = "the second bonus rolls after a strike in the last frame can not be a strike if the first one is not a strike"
, rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 6, 10]
, expected = Nothing
, expected = Left $ InvalidRoll 20 10
}
, Case { description = "second bonus roll after a strike in the last frame can not score than 10 points"
, rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 10, 11]
, expected = Nothing
, expected = Left $ InvalidRoll 20 11
}
, Case { description = "an unstarted game can not be scored"
, rolls = []
, expected = Nothing
, expected = Left IncompleteGame
}
, Case { description = "an incomplete game can not be scored"
, rolls = [0, 0]
, expected = Nothing
, expected = Left IncompleteGame
}
, Case { description = "a game with more than ten frames can not be scored"
, rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
, expected = Nothing
, expected = Left $ InvalidRoll 20 0
}
, Case { description = "bonus rolls for a strike in the last frame must be rolled before score can be calculated"
, rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10]
, expected = Nothing
, expected = Left IncompleteGame
}
, Case { description = "both bonus rolls for a strike in the last frame must be rolled before score can be calculated"
, rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 10]
, expected = Nothing
, expected = Left IncompleteGame
}
, Case { description = "bonus roll for a spare in the last frame must be rolled before score can be calculated"
, rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 3]
, expected = Nothing
, expected = Left IncompleteGame
}
]