# Guess a random number

(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

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.

```{- 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:xs) = 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
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
| target > guess = do
putStrLn "Too low"
guessFor target \$ attempts + 1
| otherwise = do
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
(\inchar -> inchar `elem` "yYnN")
(\inchar -> toUpper inchar)

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

-- 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 ()

-- 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? "
if again == 'Y'
then return True
else return False

quitGame :: IO ()
quitGame = do
exitWith ExitSuccess

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

exitWithBadArgs :: IO ()
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) = (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)```