Guess a random number

From HaskellWiki
Revision as of 15:17, 6 February 2021 by Gwern (talk | contribs) (Reverted edits by Tomjaguarpaw (talk) to last revision by DonStewart)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
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)