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   |", "  /|\\  |", "  / \\  |", "       |", " ========="]
```
\$\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

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

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.