packages feed

HCL-1.3: hangman/Hangman.hs

import System.Console.HCL
import Data.Set (Set)
import qualified Data.Set as Set
import Data.Char
import System.Random (getStdRandom, randomR)
import Control.Exception (bracket)
import System.IO (openFile, hClose, IOMode (ReadMode), hGetContents, hFileSize)
import Data.List (genericDrop)
import qualified Paths_HCL as Path -- Necessary because of cabal packaging.

-- Stores info for the hangman game, including
-- the word being guessed, letters used so far, and
-- correctly guessed letters (and positions) so far
data Hangman = Hangman {
  numTurns :: Int,
  word :: String,
  lettersUsed :: Set Char,
  turn :: Int }

-- Make an initial hangman value.
makeHangman turns goal = Hangman turns goal Set.empty 0

-- Select a random word from those in the file "2of12.txt"
-- Tries to be fancy and lazy by potentially reading past the end
-- of the fail and scaling back.
selectWord :: IO String
selectWord =
  do
    wordFile <- Path.getDataFileName "hangman/2of12.txt"
    bracket (openFile wordFile ReadMode) hClose getWord
  where
    -- Select a word from the file. Start with file size / 2
    -- as our estimate of number of words
    getWord f =
      do
        sz <- hFileSize f 
        wordList <- hGetContents f
        pickLine (sz `div` 2) (lines wordList)
    pickLine max words = 
      do
        whichLine <- getStdRandom (randomR (0, max))
        if null $ genericDrop (whichLine - 1) words
          then pickLine (max `div` 2) words
          else return $ head (genericDrop (whichLine - 1) words)

-- Add a guess to the hangman game.
addGuess :: Hangman -> Char -> Hangman
addGuess h@(Hangman {lettersUsed = letters, turn = currTurn, word = currWord}) char =
  if char `elem` currWord
    then h { lettersUsed = (Set.insert char letters) }
    else h { lettersUsed = (Set.insert char letters), turn = (currTurn + 1) }

hangManPic :: Bool -> [String]
hangManPic final = 
    "    _______    " :
    "   /      |     " :
    "   |     ___     " :
    eyes :
    "   |    |   |     " :
    "   |     \\_/      " :
    "   |      |      " :
    "   |     /|\\      " :
    "   |    / | \\     " :
    "   |      |       " :
    "   |      |      " :
    "   |      |       " :
    "   |     / \\       " :
    "   |    /   \\     " :
    "   |             " :
    "----------------" : []
  where
    eyes :: String
    eyes =
      if final
      then "   |    /x x\\     " 
      else "   |    /   \\     " 

-- Print current state of Hangman game.
printHangMan :: Hangman -> IO ()
printHangMan h =
  do
    putStrLn ""
    putStrLn $ formatHangman h
    putStrLn $ "Turns remaining: " ++ show ((numTurns h) - (turn h))
    putStrLn $ "Letters used: " ++ (formatLettersUsed h)
    putStrLn $ "Guess: " ++ (formatGuess h)
  where
    formatLettersUsed :: Hangman -> String
    formatLettersUsed h = foldr (\c rest -> c : " " ++ rest) "" (Set.elems $ lettersUsed h)
    formatGuess :: Hangman -> String
    formatGuess h =
        foldr (\l r -> l : ' ' : r) "" formatGuess 
      where
        formatGuess = map (\c -> if letterGuessed c then c else '_') (word h)
        letterGuessed c = c `Set.member` (lettersUsed h)    
    formatHangman :: Hangman -> String
    formatHangman (Hangman numTurns _ _ turn) =
      let
          picture = hangManPic False
          picLength = toRational $ length picture
          picRatio = picLength / (toRational numTurns)
          amount = (toRational turn) * picRatio
      in
        -- Display last amount lines, since we want to show hangman from bottom up
        unlines $ drop (floor (picLength - amount)) picture
  
-- Plays a round of hangman.
playRound :: Hangman -> Request Hangman
playRound hangMan = 
  do
    nextTurn hangMan
    guess <- reqGuess
    return (addGuess hangMan guess)
  where
    -- Get the next guess from the user. If a letter is entered which already exists,
    -- the user is asked to guess again.
    reqGuess = reqUntil newGuess reqLetter
    -- Test which determines if the character entered has been guessed before.
    newGuess char = return (not $ char `Set.member` (lettersUsed hangMan))
    -- Gets a single letter from the user. If a line of text is entered, only
    -- the first value is used. Loops until valid input is entered. Return/spaces will
    -- cancel though.
    reqLetter :: Request Char
    reqLetter =
      reqLift head $
        reqUntil validGuess (prompt ("Guess a letter: ") reqResp)
    -- Test which determines if the character entered is valid.
    validGuess [] = return False
    validGuess (s:str) = return (isLetter s)
    -- Processing for the next turn of hangman.
    nextTurn hangMan =
      do
        if wordGuessed hangMan
         then do { reqIO $ putStrLn ("You win! The word was " ++ word hangMan); reqFail }
         else
           if (turn hangMan >= numTurns hangMan)
             then do { reqIO $ sequence [putStrLn $ unlines (hangManPic True), putStrLn ("You lose! The word was " ++ word hangMan)]; reqFail }
             else reqIO $ printHangMan hangMan 
    -- Determine if the word has been guessed yet.    
    wordGuessed :: Hangman -> Bool
    wordGuessed h = and (map (`Set.member` (lettersUsed h)) (word h))

-- Picks a word and starts the hangman game.
main :: IO ()
main =
  do
    putStrLn "Hangman uses the excellent word list 2of12.txt compiled by Alan Beale (biljir@pobox.com). Let's play!"
    word <- selectWord 
    execReq $ reqIterate playRound (makeHangman 10 word)