Chapter 13
module Chapter13 where
import Control.Monad (forever)
import Data.Char (toLower)
import Data.List (intersperse)
import Data.Maybe (isJust)
import System.Exit (exitSuccess)
import System.IO (BufferMode(NoBuffering), hSetBuffering, stdout)
import System.Random (randomRIO)
import Test.Hspec
newtype WordList =
Wordlist [String]
deriving (Eq, Show)
allWords :: IO WordList
allWords = do
dict <- readFile "/usr/share/dict/words"
return . Wordlist $ (lines dict)
minWordLength :: Int
minWordLength = 5
maxWordLength :: Int
maxWordLength = 9
gameWords :: IO WordList
gameWords = do
(Wordlist aw) <- allWords
return . Wordlist $ (filter gameLength aw)
where
gameLength w =
let l = length w
in l >= minWordLength && l < maxWordLength
randomWord :: WordList -> IO String
randomWord (Wordlist wl) = do
randomIndex <- randomRIO (0, length wl - 1)
return $ wl !! randomIndex
randomWord' :: IO String
randomWord' = gameWords >>= randomWord
data Puzzle =
Puzzle String
[Maybe Char]
[Char]
deriving (Eq)
instance Show Puzzle where
show (Puzzle _ discovered guessed) =
(intersperse ' ' $ fmap renderPuzzleChar discovered) ++
" Guessed so far: " ++ guessed
renderPuzzleChar :: Maybe Char -> Char
renderPuzzleChar Nothing = '_'
renderPuzzleChar (Just c) = c
freshPuzzle :: String -> Puzzle
freshPuzzle word = Puzzle word (map (const Nothing) word) []
charInWord :: Puzzle -> Char -> Bool
charInWord (Puzzle word _ _) c = elem c word
alreadyGuessed :: Puzzle -> Char -> Bool
alreadyGuessed (Puzzle _ _ guessed) c = elem c guessed
fillInCharacter :: Puzzle -> Char -> Puzzle
fillInCharacter (Puzzle word filledInSoFar s) c =
Puzzle word newFilledInSoFar (c : s)
where
zipper guessed wordChar guessChar =
if wordChar == guessed
then Just wordChar
else guessChar
newFilledInSoFar = zipWith (zipper c) word filledInSoFar
handleGuess :: Puzzle -> Char -> IO Puzzle
handleGuess puzzle guess = do
putStrLn $ "Your guess was: " ++ [guess]
case (charInWord puzzle guess, alreadyGuessed puzzle guess) of
(_, True) -> do
putStrLn "You already guessed that character, pick something else!"
return puzzle
(True, _) -> do
putStrLn "This character was in the word, filling in the word accordingly"
return (fillInCharacter puzzle guess)
(False, _) -> do
putStrLn "This character wasn't in the word, try again"
return (fillInCharacter puzzle guess)
isGameOver :: Puzzle -> Bool
isGameOver (Puzzle wordToGuess _ guessed) =
foldr countIfNotInWord 0 guessed >= 7
where
countIfNotInWord c acc
| elem c wordToGuess = acc
| otherwise = acc + 1
gameOver :: Puzzle -> IO ()
gameOver puzzle@(Puzzle wordToGuess _ guessed) =
if isGameOver puzzle
then do
putStrLn "You lose!"
putStrLn $ "The word was: " ++ wordToGuess
exitSuccess
else return ()
gameWin :: Puzzle -> IO ()
gameWin (Puzzle _ filledInSoFar _) =
if all isJust filledInSoFar
then do
putStrLn "You win!"
exitSuccess
else return ()
runGame :: Puzzle -> IO ()
runGame puzzle =
forever $ do
gameOver puzzle
gameWin puzzle
putStrLn $ "Current puzzle is: " ++ show puzzle
putStr "Guess a letter: "
guess <- getLine
case guess of
[c] -> handleGuess puzzle c >>= runGame
_ -> putStrLn "Your guess must be a single character"
main' :: IO ()
main' = do
hSetBuffering stdout NoBuffering
word <- randomWord'
let puzzle = freshPuzzle (fmap toLower word)
runGame puzzle
spec :: SpecWith ()
spec = do
describe "freshPuzzle" $ do
it "for hello" $
freshPuzzle "hello" `shouldBe`
Puzzle "hello" [Nothing, Nothing, Nothing, Nothing, Nothing] []
describe "charInWord" $ do
it "for found" $ charInWord (freshPuzzle "hello") 'h' `shouldBe` True
it "for not found" $ charInWord (freshPuzzle "hello") 'z' `shouldBe` False
describe "alreadyGuessed" $ do
it "for found" $
alreadyGuessed (Puzzle "h" [Nothing] ['z']) 'z' `shouldBe` True
it "for not found" $
alreadyGuessed (Puzzle "h" [Nothing] []) 'h' `shouldBe` False
describe "isGameOver" $ do
it "for less than 7 guesses" $
isGameOver (Puzzle "h" [Nothing] "abcdef") `shouldBe` False
it "for 7 incorrect guesses" $
isGameOver (Puzzle "h" [Nothing] "abcdefg") `shouldBe` True
it "for 6 incorrect guesses and a correct guess" $
isGameOver (Puzzle "hz" [Just 'h', Nothing] "abcdefh") `shouldBe` False
describe "renderPuzzleChar" $ do
it "for Nothing" $ renderPuzzleChar Nothing `shouldBe` '_'
it "for a letter" $ renderPuzzleChar (Just 'h') `shouldBe` 'h'
describe "fillInCharacter" $ do
it "for not found" $
fillInCharacter (Puzzle "hi" [Nothing, Nothing] "") 'z' `shouldBe`
(Puzzle "hi" [Nothing, Nothing] "z")
it "for found" $
fillInCharacter (Puzzle "hi" [Nothing, Nothing] "") 'h' `shouldBe`
(Puzzle "hi" [Just 'h', Nothing] "h")