Guess a random number
Jump to navigation
Jump to search
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)