Guess a random number

From HaskellWiki
Revision as of 18:52, 7 October 2006 by BrettGiles (talk | contribs) (GuessRandom moved to Guess a random number)
Jump to navigation Jump to search
The printable version is no longer supported and may have rendering errors. Please update your browser bookmarks and please use the default browser print function instead.

This program started as an experiment in how to work with a random number generator with a known seed, so that results would be reproducible. The seed had to be either user-specified or itself randomly generated. Along the way, it became a game. At this point, it also demonstrates simple interaction with the environment (prompting users, getting command-line arguments, exiting explicitely, etc).

There is nothing fancy or mind-blowing about it; it's my first Haskell program, and I just hope it can help out other newbies. Comments, criticism, and rewrites are welcome. Thanks to #haskell for the advice they've given.


{- A simple 'guess the random number' game: 
 - this demonstrates a use of I/O and,
 - more importantly, random numbers in Haskell.
 -}

import Char
import Data.Maybe
import System.Environment
import System.Exit
import Random

maxNum = 100

main :: IO ()
main = do
  args <- getArgs
  verifyArgsOrQuit args
  seed <- getSeed args
  showSeed seed
  playGame $ getRandomGen seed
  putStrLn "Game over"
	
-- create a random generator with the specified seed value
getRandomGen :: Int -> StdGen
getRandomGen seed = mkStdGen seed


-- If a seed is specified, use it; otherwise, pick a random one
-- This is a little ugly: a seed is initially in the form ["123"],
-- which is how it's represented as an argument to the program
getSeed :: [String] -> IO Int
getSeed [] = getRandomSeed
getSeed (x:_) = return $ read x

-- Use the pre-seeded random generator to get a random seed
-- for another random generator if none was specified by the user.
-- This is needed as I couldn't find a way to get the seed
-- out of an existing random generator (such as the system one),
-- yet I needed to be able to tell the user what the seed was,
-- so that the game would be repeatable.
getRandomSeed :: IO Int
getRandomSeed = do 
  randomSrc <- getStdGen
  return $ fst $ Random.random $ randomSrc

-- A top-level wrapper for actually playing the game.
playGame :: StdGen -> IO ()
playGame randomGen = do 
  putStrLn $ "\nGuess the number (between 0 and " ++ (show (maxNum - 1)) ++ ")"
  let (rawTargetNum, nextGen) = next randomGen
  let target = rawTargetNum `mod` maxNum
  guessFor target 0
  showAnswer target
  again <- playAgain
  if again
     then playGame nextGen
     else quitGame

-- guessFor handles all of the actual guesses during a game.
guessFor :: Int -> Int -> IO ()
guessFor target attempts =  do 
  putStr "Current guess? "
  guess <- getNum "\nCurrent guess? "
  if target == guess
     then guessCorrect $ attempts + 1
     else guessWrong target attempts guess 

guessCorrect :: Int -> IO ()
guessCorrect numTries = do 
  putStrLn $ "You won in " ++ show numTries ++ " guesses." 

guessWrong :: Int -> Int -> Int -> IO ()
guessWrong target attempts guess = do
  if target > guess
     then putStrLn "Too Low"
     else putStrLn "Too high"
  guessFor target $ attempts + 1

-- The rest of the code is I/O oriented: getting user input, 
-- and small wrappers to display stuff

-- Prompt until a valid Y / N (case-insensitive) is read, and return it.
getYN :: String -> IO Char
getYN promptAgain = 
  getFromStdin promptAgain getChar (`elem` "yYnN") toUpper

-- Prompt until a valid number is read, and return it
getNum :: String -> IO Int
getNum promptAgain = 
  getFromStdin promptAgain getLine isNum read

-- This contains the logic common to getNum and getYN;
-- it repeatedly prompts until input matching some criteria
-- is given, transforms that input, and returns it
getFromStdin :: String -> (IO a) -> (a -> Bool) -> (a -> b) -> IO b
getFromStdin promptAgain inputF isOk transformOk = do
  input <- inputF
  if isOk input
     then return $ transformOk input
     else do
       putStr promptAgain
       getFromStdin promptAgain inputF isOk transformOk



showSeed :: Int -> IO ()
showSeed seed = putStrLn $ "The random seed is " ++ show seed

showAnswer :: Int -> IO ()
showAnswer answer = putStrLn $ "The answer was " ++ show answer

-- Ask if the user wants to play again; 
-- getYN always returns an uppercase letter, so the check is sufficient
playAgain :: IO Bool
playAgain = do 
  putStr "Play again? "
  again <- getYN "\nPlay again? "
  return $ again == 'Y' 

quitGame :: IO ()
quitGame = do 
  putStrLn "\nEnough already."
  exitWith ExitSuccess


-- Argument verification code
verifyArgsOrQuit :: [String] -> IO ()
verifyArgsOrQuit args =
  if verifyArgs args
     then putStrLn "args ok!"
     else exitWithBadArgs

exitWithBadArgs :: IO ()
exitWithBadArgs = do 
  progName <- getProgName
  putStrLn $ "Use: " ++  progName ++ " [optional random seed]"
  exitWith $ ExitFailure 1

-- Legitimate arguments are none, or a string representing
-- a random seed.  Nothing else is accepted.
verifyArgs :: [String] -> Bool
verifyArgs [] = True
verifyArgs (x:xs) = null xs && isNum x

-- Verify that input is a number.  This approach was chosen as read raises an
-- exception if it can't parse its input.  This approach has the benefit
-- of being short, yet sufficient to allow the use of read on anything verified
-- with it, without having to deal with exceptions.
isNum :: String -> Bool
isnum [] = False 
isNum (x:xs) = all isDigit xs && (x == '-' || isDigit x)