Difference between revisions of "Guess a random number"

From HaskellWiki
Jump to navigation Jump to search
(Deleting page that hasn't been edited for over 10 years)
m (Reverted edits by Tomjaguarpaw (talk) to last revision by DonStewart)
 
Line 1: Line 1:
  +
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.
  +
  +
  +
<haskell>
  +
{- 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)
  +
  +
</haskell>
  +
  +
[[Category:Code]]

Latest revision as of 15:17, 6 February 2021

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)