Difference between revisions of "Guess a random number"
Jump to navigation
Jump to search
Tomjaguarpaw (talk | contribs) (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)