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 2 options:

1. Load it into Hugs or GHCi, and type "main".

2. 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 <- try (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.
-}

-----------------------------------------------------------------------