module Main where import System.IO import System.IO.Error ----------------------------------------------------------------------- {- This is a very simple implementation of a very simple game. It is called "The Zoo". The user thinks of an animal, and the computer can ask yes/no questions to guess the animal. When the computer guesses wrong, the user helps the computer by providing what animal she was thinking of, and what question the computer should ask to get to that animal. In this way, the computer "learns" more and more animals, and what questions should be asked to guess the correct animal. Example play: Think of an animal! I will ask you questions about it. Does it have a trunk? no Does it swim in the sea? no Is it an insect? yes My guess: Is it a ant? no Congrats! You won. Just curious: What was your animal? butterfly Give me a question for which the answer for "butterfly" is "yes" and the answer for "ant" is "no". > Does it have wings? The next time around, the game might go as follows: Think of an animal! I will ask you questions about it. Does it have a trunk? no Does it swim in the sea? no Is it an insect? yes Does it have wings? yes My guess: Is it a butterfly? yes Hurray! I won. -} {- Possible extensions, for you to do as an exercise if you want to: * (easy) Ask a question at the end, for the game to repeat itself when it is over. * (easy) Solve the following problem: The computer asks "is it a ant?", although correct English would be to say "is it an ant?". * (easy) The question provided by the user has to have the answer "yes" for the user's animal and "no" for the computer's animal. Make the game more flexible to also allow for "no" and "yes", respectively, instead. * (easy) Allow the possibility for multiple zoo-files. The user can choose which file to use at the beginning of the game. * (easy) In readZoo, check if the file actually contains a Zoo. If not, revert to the defaultZoo (just as when there exists no zoo-file), or print an error message on the screen. (Hint: use the function readIO) * (easy) Add functionality for the user to remove all animals in the zoo, and start over from scratch. * (harder) Make the Zoo editable, so that when the user has made a mistake, it can be fixed. Questions could be changed, and animals removed. * (harder) Make a GUI to this game, where the user can press Yes and No buttons instead of typing in the answers. * (very hard) The decision tree can grow quite unbalanced. Instead of using a decision tree, one can simply keep track of all animals we have seen so far, and what yes/no answers we have seen for what questions. At each point during the game, the computer can decide the "best" question to ask; namely the question that divides the set of known animals into two groups most evenly. -} {- For some technical discussions on IO-related things, see the end of this file. -} {- To run this file, there are three options: 1. Load it into GHCi, and type "main". 2. Run it from the terminal using "runhaskell Zoo". 3. Compile this program by saying "ghc --make Zoo -o zoo" and the execute the resulting program by typing "zoo" or "./zoo". -} ----------------------------------------------------------------------- {- A simple datatype for keeping track of a "yes/no decision tree" for animals. -} data Zoo = Animal String | Question String Zoo Zoo deriving ( Show, Read, Eq ) -- writeZoo stores the zoo in a file writeZoo :: FilePath -> Zoo -> IO () writeZoo file zoo = do writeFile file (show zoo) -- readZoo restores a zoo from a file, returning a default zoo if the file -- does not exist readZoo :: FilePath -> IO Zoo readZoo file = do ms <- tryIOError (readFile file) case ms of Left _ -> do putStrLn ( "(could not read zoo-file " ++ show file ++ " -- using the default zoo)" ) return defaultZoo Right s -> do return (read s) -- the defaultZoo is used when no zoo-file exists defaultZoo :: Zoo defaultZoo = Animal "monkey" ----------------------------------------------------------------------- {- Playing the game: We are walking down the decision tree, constructing an updated zoo as we go. There are two cases: * When we are at a question, the question is asked, and we continue down the right path recursively. We construct the updated zoo on our way back. * When we arrive at an animal, we guess that that is the animal. If not, we prompt the user for what animal she was thinking of, and what question differentiates between her animal and our animal. We then return an updated version of the zoo. -} -- play zoo asks questions according to the zoo, and returns an updated -- version of the zoo. play :: Zoo -> IO Zoo play (Question quest yes no) = do b <- yesNoQuestion quest if b then do yes' <- play yes return (Question quest yes' no) else do no' <- play no return (Question quest yes no') play (Animal anim) = do b <- yesNoQuestion ("My guess: Is it a " ++ anim ++ "?") if b then do putStrLn "Hurray! I won." return (Animal anim) else do putStrLn "Congrats! You won." anim' <- question "Just curious: What was your animal?" putStrLn ( "Give me a question for which the answer for " ++ show anim' ++ " is " ++ show "yes" ++ " and the answer for " ++ show anim ++ " is " ++ show "no" ++ "." ) quest <- question ">" return (Question quest (Animal anim') (Animal anim)) ----------------------------------------------------------------------- {- Question-asking helper functions. -} -- question quest asks the question quest, returning the answer question :: String -> IO String question quest = do putStr (quest ++ " ") hFlush stdout getLine -- yesNoQuestion quest asks the question quest, not accepting any -- other answer than yes or no yesNoQuestion :: String -> IO Bool yesNoQuestion quest = do ans <- question quest case ans of "yes" -> return True "no" -> return False _ -> yesNoQuestion "Please answer yes or no!" ----------------------------------------------------------------------- {- The main function. -} -- main reads in a zoo from a file, plays the game once, and stores -- the updated zoo in the file. main :: IO () main = do putStrLn "Think of an animal! I will ask you questions about it." zoo <- readZoo zooFile zoo' <- play zoo writeZoo zooFile zoo' -- the zoo file zooFile :: FilePath zooFile = "animals.zoo" ----------------------------------------------------------------------- {- On the use of "try". The function "try" comes from the module System.IO.Error and has the following type: try :: IO a -> IO (Either IOError a) It allows us to see if executing an IO instruction generated an error or not. Normally, reading from a file that does not exist generates an error, and the program terminates. We don't want to terminate; instead we want to continue with a default zoo. Using try, we turn an IO instruction that possibly generates an error into an IO instruction that never generates any error, but instead we can look at the result and see if an error occurred. Look at the documentation for the datatypes Either and IOError for more information. -} ----------------------------------------------------------------------- {- On the use of "hFlush". Most operating systems do not write things directly on the screen when the program tells them to. Instead, they wait until a whole line is printed to the screen, and then they actually print it. This can be a nuisance when a question is printed on the screen, and the user has to type the answer on the same line (as we want in this game). To avoid the question not being printed on the screen, we tell the operating system to "flush" the output of the program, i.e. print everything on the screen that we want to print on the screen, not waiting for the end of the current line. The types of the functions involved are: hFlush :: Handle -> IO () stdout :: Handle If we want to "flush" the output of the program, we use hFlush stdout. There are other things that can be flushed (of type Handle). The details of that are not important here. -} -----------------------------------------------------------------------