Guess a random number
From HaskellWiki
(Difference between revisions)
m (improved pattern matching on getSeed on the suggestion of dmhouse, #haskell) |
(Applied most non-fmap changes suggested by #haskell.) |
||
| 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). | 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. | + | 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. |
| Line 74: | Line 74: | ||
guessCorrect :: Int -> IO () | guessCorrect :: Int -> IO () | ||
guessCorrect numTries = do | guessCorrect numTries = do | ||
| - | putStrLn $ "You won in " ++ | + | putStrLn $ "You won in " ++ show numTries ++ " guesses." |
guessWrong :: Int -> Int -> Int -> IO () | guessWrong :: Int -> Int -> Int -> IO () | ||
| - | guessWrong target attempts guess | + | 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, | -- The rest of the code is I/O oriented: getting user input, | ||
| Line 91: | Line 89: | ||
getYN :: String -> IO Char | getYN :: String -> IO Char | ||
getYN promptAgain = | getYN promptAgain = | ||
| - | getFromStdin promptAgain | + | getFromStdin promptAgain getChar (`elem` "yYnN") toUpper |
| - | + | ||
| - | + | ||
| - | + | ||
-- Prompt until a valid number is read, and return it | -- Prompt until a valid number is read, and return it | ||
getNum :: String -> IO Int | getNum :: String -> IO Int | ||
getNum promptAgain = | getNum promptAgain = | ||
| - | getFromStdin promptAgain | + | getFromStdin promptAgain getLine isNum read |
| - | + | ||
| - | + | ||
| - | + | ||
-- This contains the logic common to getNum and getYN; | -- This contains the logic common to getNum and getYN; | ||
| Line 119: | Line 111: | ||
showSeed :: Int -> IO () | showSeed :: Int -> IO () | ||
| - | showSeed seed = putStrLn $ "The random seed is " ++ | + | showSeed seed = putStrLn $ "The random seed is " ++ show seed |
showAnswer :: Int -> IO () | showAnswer :: Int -> IO () | ||
| - | showAnswer answer = putStrLn $ "The answer was " ++ | + | showAnswer answer = putStrLn $ "The answer was " ++ show answer |
-- Ask if the user wants to play again; | -- Ask if the user wants to play again; | ||
| Line 130: | Line 122: | ||
putStr "Play again? " | putStr "Play again? " | ||
again <- getYN "\nPlay again? " | again <- getYN "\nPlay again? " | ||
| - | + | return $ again == 'Y' | |
| - | + | ||
| - | + | ||
quitGame :: IO () | quitGame :: IO () | ||
| Line 157: | Line 147: | ||
verifyArgs :: [String] -> Bool | verifyArgs :: [String] -> Bool | ||
verifyArgs [] = True | verifyArgs [] = True | ||
| - | verifyArgs (x:xs) = | + | verifyArgs (x:xs) = null xs && isNum x |
-- Verify that input is a number. This approach was chosen as read raises an | -- Verify that input is a number. This approach was chosen as read raises an | ||
| Line 166: | Line 156: | ||
isnum [] = False | isnum [] = False | ||
isNum (x:xs) = all isDigit xs && (x == '-' || isDigit x) | isNum (x:xs) = all isDigit xs && (x == '-' || isDigit x) | ||
| + | |||
</haskell> | </haskell> | ||
Revision as of 12:53, 10 June 2006
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)
