[Haskell-cafe] Hangman game

Ronald Guida ronguida at mindspring.com
Sat Jan 19 17:31:01 EST 2008


Hi,

I'm interested in learning how to program games.  Since I have to start
somewhere, I decided to write a simple Hangman game.  I'm wondering if
anyone can look at my code and give me some feedback.

Thank you.

-- Hangman game
module Main
    where

import Data.Char
import Data.List
import System.IO
import System.Random
import Control.Monad.State

data GameState = GameState {
      gsAnswer  :: String,       -- the answer
      gsKnown   :: [Maybe Char], -- partial answer known to the user
      gsGuesses :: [Char],       -- incorrect letters guessed so far
      gsWrong   :: Int,          -- number of incorrect guesses
      gsWonLost :: Maybe Bool    -- Just true = won, Just false = lost
    }
  deriving (Show)

newGameState :: String -> GameState
newGameState answer = GameState{
                   gsAnswer = map toUpper answer,
                   gsKnown = (map (filt $ not . isAlpha) answer),
                   gsGuesses = [],
                   gsWrong = 0,
                   gsWonLost = Nothing}

data UserInput = UIGuess Char | UIQuit | UINewGame | UIRefresh
  deriving (Show)

main :: IO ()
main = do
  hSetBuffering stdout NoBuffering
  putStr "Welcome to Hangman!\n\n"
  putStr instructions
  runStateT startNewGame undefined
  return ()

startNewGame :: StateT GameState IO ()
startNewGame = do
  nWord <- liftIO $ getStdRandom (randomR (0,length wordList - 1))
  let word = wordList !! nWord
  let gs = newGameState word
  put gs
  liftIO $ putStrLn $ renderGameState gs
  gameLoop

gameLoop :: StateT GameState IO ()
gameLoop = do
  ui <- liftIO getUserInput
  case ui of
    UIGuess c -> do
      modify $ handleGuess c
      gs <- get
      liftIO $ putStrLn $ renderGameState gs
      case (gsWonLost gs) of
        Nothing -> gameLoop
        Just True -> do
          liftIO $ putStrLn "Congratulations, you won!"
          startNewGame
        Just False -> do
          liftIO $ putStrLn "You've been hanged!"
          liftIO $ putStrLn $ "The word was \'" ++ (gsAnswer gs) ++
"\'."
          startNewGame
    UIQuit -> do
      gs <- get
      liftIO $ putStrLn $ "The word was \'" ++ (gsAnswer gs) ++ "\'."
      liftIO $ putStrLn "Thank you for playing!"
    UINewGame -> do
      gs <- get
      liftIO $ putStrLn $ "The word was \'" ++ (gsAnswer gs) ++ "\'."
      startNewGame
    UIRefresh -> do
      gs <- get
      liftIO $ putStrLn $ renderGameState gs
      gameLoop

getUserInput :: IO UserInput
getUserInput = do
  putStr "Hangman> "
  response <- getLine
  if null response
    then getUserInput
    else do
      let c:cs = response
      if isAlpha c
        then return $ UIGuess $ toUpper c
        else if c == ':' && not (null cs)
          then case toLower (head cs) of
                 'q' -> return UIQuit
                 'n' -> return UINewGame
                 'r' -> return UIRefresh
                 '?' -> do
                   putStr instructions
                   getUserInput
                 otherwise -> do
                   putStrLn $ "Unknown command \'" ++ cs ++ "\'"
                   putStrLn $ "Use \':?\' for help."
                   getUserInput
          else do
            putStrLn $ "Invalid input \'" ++ response ++ "\'"
            putStrLn $ "Use \':?\' for help."
            getUserInput

instructions :: String
instructions =
    "Instructions:\n"
 ++ "To guess a letter, type the letter and press enter.\n"
 ++ "To quit or restart the game, use the following commands:\n"
 ++ "  :q = quit\n"
 ++ "  :n = new game\n"
 ++ "  :r = re-display the game state\n"
 ++ "  :? = show instructions\n"
 ++ "\n"

filt :: (a -> Bool) -> a -> Maybe a
filt pred x = if pred x then Just x else Nothing

handleGuess :: Char -> GameState -> GameState
handleGuess ch state =
    if (elem ch $ gsGuesses state)
      then state
      else
    if (elem ch $ gsAnswer state)
      then let revealed = map (filt (== ch)) (gsAnswer state)
               known = zipWith mplus (gsKnown state) revealed
               won = all (maybe False (const True)) known
           in state{gsKnown = known, gsWonLost = filt id won}
      else let wrong = 1 + (gsWrong state)
           in state{gsGuesses = ch:(gsGuesses state),
                    gsWrong = wrong,
                    gsWonLost = filt not (wrong < 7)}

wordList :: [String]
wordList = ["alligator", "angelfish", "ant", "bear", "buffalo",
            "butterfly", "canary", "chameleon", "crab", "dinosaur",
            "dog", "dolphin", "eel", "elephant", "flamingo", "frog",
            "giraffe", "goldfish", "grasshopper", "hedgehog",
            "hippopotamus", "horse", "iguana", "jaguar", "jellyfish",
            "kangaroo", "kinkajou", "lemur", "lizard", "llama",
            "meerkat", "moose", "mouse", "narwhal", "nautilus",
            "nuthatch", "ostrich", "owl", "panda", "pelican",
            "quail", "quokka", "raccoon", "rhinoceros", "salamander",
            "sea horse", "sea urchin", "snail", "tiger", "toucan",
            "uakari", "unicorn", "vampire bat", "vulture", "walrus",
            "wildebeest", "worm", "xenops", "yak", "yellow jacket",
            "zebra"]

renderGameState :: GameState -> String
renderGameState gs =
    let noose = renderNoose $ gsWrong gs
        report = ["","The Word:","",word,"","Your Guesses:","",guessed]
        word = intersperse ' ' $ map (maybe '_' id) (gsKnown gs)
        guessed = gsGuesses gs
    in (concat $ zipWith (++) noose $ map (++ "\n") report)

renderNoose :: Int -> [String]
renderNoose n | n <= 0 = [
 "   ___     ",
 "  /   |    ",
 "  |        ",
 "  |        ",
 "  |        ",
 "  |        ",
 "  |        ",
 " -+-       "]
renderNoose 1 = [
 "   ___     ",
 "  /   |    ",
 "  |   O    ",
 "  |        ",
 "  |        ",
 "  |        ",
 "  |        ",
 " -+-       "]
renderNoose 2 = [
 "   ___     ",
 "  /   |    ",
 "  |   O    ",
 "  |   |    ",
 "  |        ",
 "  |        ",
 "  |        ",
 " -+-       "]
renderNoose 3 = [
 "   ___     ",
 "  /   |    ",
 "  |   O    ",
 "  | --|    ",
 "  |        ",
 "  |        ",
 "  |        ",
 " -+-       "]
renderNoose 4 = [
 "   ___     ",
 "  /   |    ",
 "  |   O    ",
 "  | --|--  ",
 "  |        ",
 "  |        ",
 "  |        ",
 " -+-       "]
renderNoose 5 = [
 "   ___     ",
 "  /   |    ",
 "  |   O    ",
 "  | --|--  ",
 "  |   |    ",
 "  |        ",
 "  |        ",
 " -+-       "]
renderNoose 6 = [
 "   ___     ",
 "  /   |    ",
 "  |   O    ",
 "  | --|--  ",
 "  |   |    ",
 "  |  /     ",
 "  |        ",
 " -+-       "]
renderNoose n | n >= 7 = [
 "   ___     ",
 "  /   |    ",
 "  |   O    ",
 "  | --|--  ",
 "  |   |    ",
 "  |  / \\   ",
 "  |        ",
 " -+-       "]




More information about the Haskell-Cafe mailing list