[SUMMARY] Animal Quiz (#15)

Quiz creator Jim Weirich shared this wonderful little tidbit with me:

  True and slightly off topic story:
  
  The first time I wrote a version of this program, it was on a simple
  single board Z80 computer using a FORTH-like language to program it.
  I had seeded the program with a single animal (a mouse) and called my
  wife in to try it out. I explained the program and she ran the
  program. It printed out the words "Think of an animal ...", and then
  paused for a few seconds. Then it asked "Is it a mouse?". My wife
  turned to me with a look of absolute astonishment and said "HOW did it
  know that?".
  
  Yep, she was thinking of a mouse.

Unfortunately, none of the submitted solutions were quite that all-knowing.

Everybody solved this one using pretty much the same technique. Let's go back
to Jim for an explanation of the strategy:

  There is an easy solution that represents the database as a binary
  tree with questions as interior nodes and possible animals as leaf
  nodes. Each interior question node has two children corresponding to
  a yes or no answer. The children are either further questions (which
  will be asked) or an animal (which will be guessed).

Couldn't have said it better myself. Let's see Jim's own implementation of said
tree:

  #!/usr/bin/env ruby
  
  require 'yaml'
  require 'ui'
  
  def ui
    $ui ||= ConsoleUi.new
  end
  
  class Question
    def initialize(question, yes, no)
    @question = question
    @yes = yes
    @no = no
    @question << "?" unless @question =~ /\?$/
    @question.sub!(/^([a-z])/) { $1.upcase }
    end
  
    def walk
    if ui.ask_if @question
      @yes = @yes.walk
    else
      @no = @no.walk
    end
    self
    end
  end
  
  class Animal
    attr_reader :name
    def initialize(name)
    @name = name
    end
  
    def walk
    if ui.ask_if "Is it #{an name}?"
      ui.say "Yea! I win!\n\n"
      self
    else
      ui.say "Rats, I lose"
      ui.say "Help me play better next time."
      new_animal = ui.ask "What animal were you thinking of?"
      question = ui.ask "Give me a question " +
    "to distinguish a #{an name} from #{an new_animal}."
      response = ui.ask_if "For #{an new_animal}, " +
    "the answer to your question would be?"
      ui.say "Thank you\n\n"
      if response
    Question.new(question, Animal.new(new_animal), self)
      else
    Question.new(question, self, Animal.new(new_animal))
      end
    end
    end
  
    def an(animal)
    ((animal =~ /^[aeiouy]/) ? "an " : "a ") + animal
    end
  end
  
  if File.exist? "animals.yaml"
    current = open("animals.yaml") { |f| YAML.load(f.read) }
  else
    current = Animal.new("mouse")
  end
  
  loop do
    current = current.walk
    break unless ui.ask_if "Play again?"
    ui.say "\n\n"
  end
  
  open("animals.yaml", "w") do |f| f.puts current.to_yaml end

This is a very straight forward solution. At the top, you can see that it
brings in YAML for storage (many people did this) and a "ui" library to handle
interface. It also defines a helper method for the ui library, making it
trivial to change the entire interface just by setting a global variable.

Skip over the class definitions now and have a look at the "main" section. The
first third loads an existing animal tree, if one is available. Otherwise, it
creates a new tree by magically predicting what Jim's wife would guess.

The middle third walk()s the tree, saving the result in case a new node is
added. It then asks if the user would like to play again, using the ui() helper
method.

The last third/line, just saves out the tree as it stands at the end of this
run. Isn't YAML handy?

To make sense of all this talk about a "tree", you need to go back up and
examine the two classes. As described in the strategy quote above, Question
objects just hold the question itself, and links to the answer nodes for yes and
no. The real method of interest here is Question#walk. walk() asks its
question through ui(), then recurses into @yes.walk() or @no.walk(), depending
on the answer provided. The trick to note here is that the result of the call
is saved back to the node. That allows nodes to update themselves, when the
game learns a new animal.

That just leaves Animal, which is even easier to grasp. Again, the method of
interest is Animal#walk. walk() guesses the animal over ui() and declares
victory if it's right. When it's wrong, it asks the clarifying questions to
learn and returns itself and the new animal wrapped in a new Question object.
This return ensures that the tree is updated, thanks to the saving behavior of
Question#walk.

That leaves only the mystical ui library. Here's a look at it:

  #!/usr/bin/env ruby
  
  class ConsoleUi
    def ask(prompt)
    print prompt + " "
    answer = gets
    answer ? answer.chomp : nil
    end
  
    def ask_if(prompt)
    answer = ask(prompt)
    answer =~ /^\s*[Yy]/
    end
  
    def say(*msg)
    puts msg
    end
  end

This is just a simple console interface, of course. ask() handles input, say()
output and ask_if() is a helper method that returns true if it looks like the
user answered with a yes or false otherwise (handy for "if" conditions, thus the
name). These methods could be replaced with CGI equivalents, GUI routines, or
whatever. Nice abstraction here.

Now, I did say the tree method was easy, but it's not without its faults. Once
more, I give you the voice of Jim:

  However, the tree solution has some draw backs. It is very sensitive
  to the order in which animals are added to the tree and the type of
  questions used. The tree solution works best when the early questions
  divide the set of possible animals into more or less equal groups.
  This keeps the tree nicely balanced and the series of questions
  leading up to any guess are all equally short. Unfortunately, in real
  life the tree tends to become very unbalanced with individual questions
  targetting a rather specific animal in the yes branch and the no
  branch becoming a long list of more specific questions.
  
  Another small problem with the tree solution is that some questions
  are ambiguous, or the user doesn't have the knowledge to answer the
  question properly. For example, a question might be "Does it live in
  the water?". Some people might select a beaver as their animal and
  think "Oh yes, it loves to swim". Others might say "No, it lives on
  land, it just enjoys swimming". In actual practice, these ambiguities
  average out and you would get the beaver answer on both yes and no
  nodes of a question, each branch using different questions to narrow
  down the choice. Although not a fatal flaw, it does put redundant
  answers in the tree and essentially waste a question that could be put
  to better use.

I actually did a fair amount of thinking about other approaches to this problem.
Unfortunately, every time I broke from the tree structure, it became a lot
trickier to add new animals. I basically had to badger the user for answers to
half of all the questions known about their animal and in doing so it seemed I
ran into even more irrelevancy issues. Because of that, I eventually abandoned
the approach. If anybody has or creates another strategy for this, be sure and
send it in!

My thanks to the submitters and Jim, who didn't realize he had already wrote
this summary when he told me I could handle it.

Tomorrow, it's every coder for themselves as we get a little friendly
competition going...