I created a HangPerson game in Haskell and I must admit it feels very imperative. How can I make it more "functional?" Are there more elegant ways of breaking up the task? The getUserLines
function seems to be doing a lot of work. Any additional feedback on efficiency or anything of the sort is also appreciated.
import Control.Monad.State
import System.Random
import Data.List
import System.Process
import Data.Char
type GameValue = String
type GameState = (Int, String)
playGame :: String -> String -> State GameState GameValue
playGame [] word = do
(_, guessed) <- get
return guessed
playGame (x:xs) word = do
(missed, guessed) <- get
put $ check missed x word guessed
playGame xs word
check n c word guessed
| not $ isAlpha c || c `elem` guessed = (n, guessed)
| c `elem` word = (n, c:guessed)
| otherwise = (n + 1, c:guessed)
replace word guess = map replace' word
where replace' char = if char `elem` guess then char else '_'
gallows n = "\n\n |||========|||\n" ++
(if n > 0 then " ||| |\n" else " ||| \n") ++
(if n > 1 then " ||| O\n" else " ||| \n") ++
(if n < 2 then " ||| \n" else (helper1 n)) ++
(if n < 5 then " ||| \n" else (helper2 n)) ++
" |||\n" ++ " |||\n" ++ " =================\n\n"
helper1 n
| n <= 3 = " ||| / \n"
| n <= 4 = " ||| /| \n"
| otherwise = " ||| /|\\\n"
helper2 6 = " ||| / \\\n"
helper2 _ = " ||| / \n"
safeHead [] = ' '
safeHead (x:xs) = x
menu = "\x1B[32m\t\t\t\tWelcome to Hangperson.\nThe rules are the same as always except\n\nIf you enter multiple characters only the first will be read.\nPress Ctrl+D to Give up.\nHave fun\x1B[0m"
getUserLines :: String -> IO String
getUserLines word = go (0, "")
where go contents = do
system "clear"
putStrLn menu
let misses = fst contents
let guessed = snd contents
putStrLn $ gallows misses
putStrLn $ "You've missed " ++ (show $ misses) ++ " out of 7"
putStrLn $ "You've guessed: " ++ "\x1B[31m" ++ guessed ++ "\x1B[0m"
putStrLn $ intersperse ' ' $ replace word guessed
putStrLn "Guess a letter: "
line <- getLine
let guess = [safeHead line]
let result = execState (playGame guess word) contents
if (((fst result) == 7) || '_' `notElem` (replace word ((snd result))))
then (if (fst result) < 7 then return ("You won! The word was " ++ word) else return ("You lost! The word was " ++ word))
else go result
main = do
system "reset"
s <- readFile "/usr/share/dict/words"
num <- randomIO :: IO Int
let word = (lines s) !! (num `mod` 230000) --(length $ lines s)) efficient way?
gameResult <- getUserLines (map toLower word)
putStrLn gameResult
putStrLn "Play again? :"
option <- getLine
if option == "y" then main else return ()
go (misses, guesses) = ...
and scrap the fst and snd lines. \$\endgroup\$