I just finished a course of functional programming in uni and continued to study Haskell because I found it very interesting! I made a simple hangman game and would like to hear any of your thoughts and ideas that may arise looking at the code! What, for example, could make it more dogmatic in a functional programming sense?
import Control.Monad
import Data.List (elemIndices, sort)
pictures = 9
main :: IO ()
main = do
word <- getWord ""
clearScreen
hang word
hang :: String -> IO ()
hang word = hang' word [] [] pictures
hang' :: String -> [Char] -> [Char] -> Int -> IO ()
hang' word _ _ lives | lives == 0 = clearScreen >> putStrLn (renderHangman 0) >> putStrLn "You lost!" >> putStrLn ("The correct word was " ++ word ++ "\n")
hang' word rights _ lives | win rights word = clearScreen >> putStrLn (renderHangman lives) >> putStrLn "You won!" >> putStrLn ("The correct word was " ++ word ++ "\n")
hang' word rights wrongs lives = do
clearScreen
putStrLn $ renderHangman lives
putStrLn $ renderWord rights word
putStrLn $ renderLives lives
putStrLn $ renderWrongs wrongs
guess <- getGuess
if guess `elem` (rights ++ wrongs)
then hang' word rights wrongs lives
else
if correctGuess guess word
then hang' word (guess : rights) wrongs lives
else hang' word rights (guess : wrongs) (lives - 1)
win :: [Char] -> String -> Bool
win guesses = all (`elem` guesses)
clearScreen :: IO ()
clearScreen = replicateM_ 100 (putStrLn "")
correctGuess :: Char -> String -> Bool
correctGuess guess word = guess `elem` word
getGuess :: IO Char
getGuess = do
putStrLn "Guess a letter!"
getChar
getWord s = do
clearScreen
putStrLn "Give a secret word!"
putStr ['*' | _ <- s]
c <- getChar
case c of
'\n' -> return s
char -> getWord (s ++ [char])
renderWord :: [Char] -> String -> String
renderWord guesses = foldr hide ""
where
hide = \x xs -> if x `elem` guesses then x : xs else '_' : xs
renderWrongs :: [Char] -> String
renderWrongs [] = ""
renderWrongs wrongs = "Wrong guesses: " ++ sort wrongs
renderHangman :: Int -> String
renderHangman = unlines . hangmanpics
renderLives :: Int -> String
renderLives lives = show lives ++ " guesses left!"
hangmanpics :: Int -> [String]
hangmanpics 9 = [" ", " ", " ", " ", " ", " ", "========="]
hangmanpics 8 = [" ", " |", " |", " |", " |", " |", "========="]
hangmanpics 7 = [" +---+", " |", " |", " |", " |", " |", "========="]
hangmanpics 6 = [" +---+", " | |", " |", " |", " |", " |", "========="]
hangmanpics 5 = [" +---+", " | |", " O |", " |", " |", " |", " ========="]
hangmanpics 4 = [" +---+", " | |", " O |", " | |", " |", " |", " ========="]
hangmanpics 3 = [" +---+", " | |", " O |", " /| |", " |", " |", " ========="]
hangmanpics 2 = [" +---+", " | |", " O |", " /|\\ |", " |", " |", " ========="]
hangmanpics 1 = [" +---+", " | |", " O |", " /|\\ |", " / |", " |", " ========="]
hangmanpics 0 = [" +---+", " | |", " O |", " /|\\ |", " / \\ |", " |", " ========="]
hangmanpics _ = [" +---+", " | |", " O |", " /|\\ |", " / \\ |", " |", " ========="]
```
1 Answer 1
looks really good!
Here are some suggestions, in rough order of significance
The game essentially can be described by the three values
word,rights,wrongs, andliveswhich you pass around between functions. For readability, I would suggest wrapping these up into a datatype:data GameState = GameState { word :: String , rights :: [Char] , wrongs :: [Char] , lives :: Int }The end of
hang'feels convoluted. By "end" I mean the code aftergetGuess. The semantic structure is this: "update" the "game state", and then restarthang'. But it's not easy to see that with the current code structure, since each branch is its own "independent" call tohang'.Storing state in its own
GameStatetype, we can cleanly split the code into the parts (1) update the state; (2) callhang'. To cut to the chase, it looks like this:guess <- getGuess let state' = if guess `elem` (rights state ++ wrongs state) then state else if correctGuess guess state then state { rights = guess : rights state } else state { wrongs = guess : wrongs state, lives = lives state - 1 } hang' state'[Char]is not quite an appropriate datatype forrightsandwrongs. Using[Char]here suggests to me that you care about the order of theChars in the list and how many times they show up, but in fact you do not. I would recommendData.Setinstead. (This will give you better performance, too!)getWordcan be implemented more simply asclearScreen >> putStrLn "Give a secret word!" >> getLinewin-- sleek implementation of this function!Unrelated, I would rename it.
winsounds like an action, but really the function is a predicate. PerhapsisWin?As a general rule of thumb, I would try to avoid unqualified imports like
import Control.Monad. If you have more than one unqualified import, it can become difficult to know where a function or operator is being imported from.getWordis missing its type signature, as ispictures. This is not a huge deal, but it's generally recommended to include type signatures on (at least) all top-level values.In some places you chain
IOactions with>>and in others you usedonotation.These are actually the same thing;
thingOne >> thingTwoanddo { thingOne; thingTwo; }are the same code. (It's unclear to me if you know this already)Anyway, for readability I would suggest using
dofor longer chains and>>or>>=for shorter expressions. Concretely, rewrite theclearScreen >> putStrLn ...parts ofhang'usingdo, and then rewritegetGuessasputStrLn "Guess a letter!" >> getChar.
Hope this helps :-) See the code with all changes made (except for using Set) here
-
1\$\begingroup\$ Oh, one more:
renderWordcan be implemented asrenderWord' guesses = foldMap (\c -> if c `elem` guesses then [c] else "_")\$\endgroup\$Quelklef– Quelklef2022年06月21日 05:02:44 +00:00Commented Jun 21, 2022 at 5:02 -
1\$\begingroup\$ This answer was definitely over the top. I agree with every bullet and can't thank you enough for seeing exactly what I was trying to do. I'll go through them one by one right away. Very kind of you to also throw in praise where you did, haha! Marking as solved. \$\endgroup\$ola_bandola– ola_bandola2022年06月21日 09:40:55 +00:00Commented Jun 21, 2022 at 9:40
-
1\$\begingroup\$ @OlaBandola Good luck! I've included a reference implementation at the end of my answer, if it's helpful \$\endgroup\$Quelklef– Quelklef2022年06月22日 16:04:29 +00:00Commented Jun 22, 2022 at 16:04