3
\$\begingroup\$

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 |", " /|\\ |", " / \\ |", " |", " ========="]
```
asked Jun 20, 2022 at 19:57
\$\endgroup\$

1 Answer 1

4
\$\begingroup\$

looks really good!

Here are some suggestions, in rough order of significance

  1. The game essentially can be described by the three values word, rights, wrongs, and lives which 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
     }
    
  2. The end of hang' feels convoluted. By "end" I mean the code after getGuess. The semantic structure is this: "update" the "game state", and then restart hang'. But it's not easy to see that with the current code structure, since each branch is its own "independent" call to hang'.

    Storing state in its own GameState type, we can cleanly split the code into the parts (1) update the state; (2) call hang'. 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'
    
  3. [Char] is not quite an appropriate datatype for rights and wrongs. Using [Char] here suggests to me that you care about the order of the Chars in the list and how many times they show up, but in fact you do not. I would recommend Data.Set instead. (This will give you better performance, too!)

  4. getWord can be implemented more simply as clearScreen >> putStrLn "Give a secret word!" >> getLine

  5. win -- sleek implementation of this function!

    Unrelated, I would rename it. win sounds like an action, but really the function is a predicate. Perhaps isWin?

  6. 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.

  7. getWord is missing its type signature, as is pictures. This is not a huge deal, but it's generally recommended to include type signatures on (at least) all top-level values.

  8. In some places you chain IO actions with >> and in others you use do notation.

    These are actually the same thing; thingOne >> thingTwo and do { thingOne; thingTwo; } are the same code. (It's unclear to me if you know this already)

    Anyway, for readability I would suggest using do for longer chains and >> or >>= for shorter expressions. Concretely, rewrite the clearScreen >> putStrLn ... parts of hang' using do, and then rewrite getGuess as putStrLn "Guess a letter!" >> getChar.

Hope this helps :-) See the code with all changes made (except for using Set) here

answered Jun 21, 2022 at 4:58
\$\endgroup\$
3
  • 1
    \$\begingroup\$ Oh, one more: renderWord can be implemented as renderWord' guesses = foldMap (\c -> if c `elem` guesses then [c] else "_") \$\endgroup\$ Commented 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\$ Commented 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\$ Commented Jun 22, 2022 at 16:04

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.