# Simple guessing game in Haskell

Here is a simple Guessing Game I implemented in Haskell

import System.Random

main :: IO ()
main = do
putStrLn "I am guessing a value between 1 to 100"
guess <- (randomIO :: IO Int)
playWith (guess mod 100)
putStrLn "Play again? (Y/N)"
response <- getLine
if response == "Y" then
main
else
return ()

playWith :: Int -> IO ()
playWith n = go 5
where
go 0 = putStrLn "You loose!"
go n = do
if n == guess then
putStrLn "You win!"
else
if n < guess then
putStrLn "Too large!" >> go (n-1)
else
putStrLn "Too small!" >> go (n-1)

s <- getLine
case (readMaybe s :: Maybe Int) of
Just x -> return x


Is there something I can (or should) do to prevent the usage of goto like recursion? Maybe I could use something from Control.Monad?

Great job on writing readUntilValid as a separate function instead of inlining it in playWith. That being said, we can go a little bit further with splitting the functionality of your code.

At the moment, your main contains five distinct steps:

• greet the user with the rules
• choose a random number
• start the game
• ask whether the user wants to play again
• restart

The random number is part of the game, so it's a little bit strange to generate it in main. Your function playWith already suggests a name for a another function that takes care of that step:

play :: IO ()
play = randomRIO (1, 100) >>= playWith


randomRIO takes a range, so we end up with the correct numbers. Your program choosed numbers between 0-99, by the way.

However, now we can also get the rules in there:

import Control.Monad (when)
import System.Random (randomRIO)

play :: IO ()
play = do
putStrLn "I am guessing a value between 1 to 100."
randomRIO (1, 100) >>= playWith

main :: IO ()
main = do
play
response <- prompt "Play again? (Y/N)"
when (response == "Y") main

prompt :: String -> IO String
prompt xs = putStr (xs ++ " ") >> getLine


The prompt is just a little helper for questions to the user. What did we gain from splitting main? Well, main does not have to care for the game at all. We can replace play by any other game, e.g. playTicTacToe, and we don't have to change our main at all. So try to isolate your code a little bit.

Next we have playWith. There's a bug in there, you use n twice, once as (correct) answer, once as the number of turns the user has. Therefore, they have to guess 5, 4, and so on. The original random number wasn't used at all. Let's fix that first:

playWith :: Int -> IO ()
where
go 0 = putStrLn "You loose!"
go n = do
putStrLn "You win!"
else
putStrLn "Too large!" >> go (n-1)
else
putStrLn "Too small!" >> go (n-1)


Note that GHC will warn you about shadowing if you use -Wall. But we can still do better. If you're going to compare two values and want to act on more than a single outcome, use compare instead of two checks (here == and <):

playWith :: Int -> IO ()
where
go 0 = putStrLn "You lost!"
go n = do

case guess compare answer of
EQ -> putStrLn "You win!"
LT -> putStrLn "Too small!" >> go (n-1)
GT -> putStrLn "Too large!" >> go (n-1)


And now back to readUntilValid As I said, it's completely fine, however the additional type signature isn't necessary, as the compiler can infer readMaybe's type in that context. We end up with:

readUntilValid :: IO Int
s <- getLine
Just x  -> return x


Note that you can even write it as

readUntilValid :: Read a => IO a


but that's an aside. We end up with

import Control.Monad (when)

main :: IO ()
main = do
play
response <- prompt "Play again? (Y/N)"
when (response == "Y") main

play :: IO ()
play = do
putStrLn "I am guessing a value between 1 to 100."
randomRIO (1, 100) >>= playWith

playWith :: Int -> IO ()
where
go 0 = putStrLn "You lost!"
go n = do

case guess compare answer of
EQ -> putStrLn "You win!"
LT -> putStrLn "Too small!" >> go (n-1)
GT -> putStrLn "Too large!" >> go (n-1)

prompt :: String -> IO String
prompt xs = putStr (xs ++ " ") >> getLine

Nothing -> putStrLn "Not a valid guess, try again." >> askNumber
Just x  -> return x


I've renamed readUntilValid and also let it ask the number, so that again everything that is concerned with getting input from the user resides in a single function.

All in all, well done. Don't be afraid to split your functions, make sure to enable compiler warnings, and use hlint (it suggested when from Control.Monad, by the way).

### Exercises

• at the moment, the user will only replay the game if they enter Y. Also accept other inputs, such as "yes", "YES" and "y", and ask again if they don't say "yes" or "no" in a clear way
• the number of guesses is currently fixed in playWith. Make it variable
• the range is currently fixed (1-100). Make it variable
• have the user choose a difficulty. The difficulty should determine the original range as well as the number of guesses. Keep the lower difficulty levels fair, e.g.

 difficulty   | range      | guesses
--------------+------------+-------------------------------
trivial      | 1-10       | 10
easy         | 1-100      | 10
medium       | 1-100      | 7
hard         | 1-1000     | 10 -- still fair
harder       | 1-10000    | 13 -- beginning to get unfair
hardest      | 1-1000000  | 17 -- missing three guesses
lucky        | 1-20       | 1

• the user doesn't know how many guesses they have. You should probably tell them at some point, especially with difficulty levels

• instead of (), playWith could return data GameEnd = Won | Lost, but that's just for style and not necessary. You could use that information for statistics and so on, but that's an overkill

Have fun.

playWith and go both call their argument n, leading to mixups.

mod maps the integers to [0.99], not [1..100].

You are looking for Control.Monad.Loops.

import Control.Monad.Loops (allM, orM, untilJust)

main = void $allM (== "Y")$ repeat $do putStrLn "I am guessing a value between 1 to 100" value <- randomRIO (1,100) :: IO Int win <- orM$ replicate 5 $do putStr "Enter your guess: " guess <- untilJust$ do
mguess <- readMaybe <$> getLine :: IO (Maybe Int) unless (isJust mguess)$ putStr "Please enter a valid guess: "
return mguess
when (guess < value) $putStrLn "Too large!" when (guess > value)$ putStrLn "Too small!"
return $guess == value putStrLn$ "You " ++ if win then "win!" else "lose!"
putStrLn "Play again? (Y/N)"
getLine

• While the code you present does have the same behaviour, it's an entirely different ballpark of complex. I don't like how you've inlined both extracted functions, while rewriting them significantly without any comment. I think it would help OP a lot if you could be significantly more explicit in your rewriting process Nov 8, 2017 at 0:08
• If extracting the inner do blocks would help, the reader can simply ignore all code that starts successively further from the left for the same effect. Nov 8, 2017 at 1:59