Skip to content

Instantly share code, notes, and snippets.

@erasmas
Created December 11, 2017 21:47
Show Gist options
  • Save erasmas/78483ec9cded2af746acbb57e234249b to your computer and use it in GitHub Desktop.
Save erasmas/78483ec9cded2af746acbb57e234249b to your computer and use it in GitHub Desktop.
Hangman exercise from Haskell Programming book
module Main where
import Control.Monad (forever)
import Data.Char (toLower)
import Data.List (intersperse)
import Data.Maybe (isJust, isNothing)
import System.Exit (exitSuccess)
import System.Random (randomRIO)
newtype WordList =
WordList [String]
deriving (Eq, Show)
minWordLength :: Int
minWordLength = 5
maxWordLength :: Int
maxWordLength = 9
allWords :: IO WordList
allWords = do
dict <- readFile "data/dict.txt"
return $ WordList (lines dict)
gameWords :: IO WordList
gameWords = do
(WordList aw) <- allWords
return $ WordList (filter gameLength aw)
where
gameLength w =
let l = length (w :: String)
in l >= minWordLength && l < maxWordLength
randomWord :: WordList -> IO String
randomWord (WordList wl) = do
randomIndex <- randomRIO (0, length wl)
return $ wl !! randomIndex
randomWord' :: IO String
randomWord' = gameWords >>= randomWord
data Puzzle =
Puzzle String
[Maybe Char]
String
renderPuzzleChar :: Maybe Char -> Char
renderPuzzleChar (Just c) = c
renderPuzzleChar Nothing = '_'
instance Show Puzzle where
show (Puzzle _ discovered guessed) =
intersperse ' ' (fmap renderPuzzleChar discovered) ++
" Guessed so far: " ++ guessed
freshPuzzle :: String -> Puzzle
freshPuzzle word = Puzzle word discovered []
where
discovered = map (const Nothing) [0 .. ((pred . length) word)]
charInWord :: Puzzle -> Char -> Bool
charInWord (Puzzle word _ _) c = c `elem` word
alreadyGuessed :: Puzzle -> Char -> Bool
alreadyGuessed (Puzzle _ _ guessed) c = c `elem` guessed
fillInCharacter :: Puzzle -> Char -> Puzzle
fillInCharacter (Puzzle word discovered guessed) c =
Puzzle word newDiscovered (c : guessed)
where
zipper guessed' wordChar guessChar =
if wordChar == guessed'
then Just wordChar
else guessChar
newDiscovered = zipWith (zipper c) word discovered
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)
wrongGuesses :: Puzzle -> Int
wrongGuesses (Puzzle _ discovered guesses) =
length guesses - rightGuesses
where
rightGuesses = length $ filter (\c -> Just c `elem` discovered) guesses
gameOver :: Puzzle -> IO ()
gameOver puzzle @ (Puzzle wordToGuess _ _) =
let maxAttempts = (pred . length) wordToGuess
wrongAttempts = wrongGuesses puzzle
attemptsLeft = maxAttempts - wrongAttempts
in if (wrongAttempts == maxAttempts)
then do
putStrLn "You lose!"
putStrLn $ "The word was: " ++ wordToGuess
exitSuccess
else do
putStrLn $ "Attempts left: " ++ (show attemptsLeft)
return ()
gameWin :: Puzzle -> IO ()
gameWin (Puzzle _ discovered _) =
if all isJust discovered
then do
putStrLn "You win!"
exitSuccess
else return ()
runGame :: Puzzle -> IO ()
runGame puzzle =
forever $ do
gameOver puzzle
gameWin puzzle
putStrLn $ "\nCurrent 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
word <- randomWord'
let puzzle = freshPuzzle (fmap toLower word)
runGame puzzle
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment