Wednesday, July 1, 2015

Game #3 - "ANIMAL" - Fool the programmer?

For our third foray into converting the games of "101 BASIC Computer Games" to Javascript, we take quite a departure from last week's AMAZING maze generator into the world of artificial intelligence. Be sure to track the original BASIC source at 101 BASIC Computer Games on Atari Archives and grab a ready-to-run set of source files including the host console window at our project at our Sourceforge archive.

So this is a halcyon effort at AI?

Kinda.

"ANIMAL" is a simple guessing game wherein the user thinks of an animal, and the *computer* tries to guess it. When the computer misses, it asks questions that differentiate its best guess from the right answer, and in so doing tries to build an "intelligence database" of sorts about the animals in that session. The result is a mixed bag.

Granted, BASIC is no panacea for advanced artificial intelligence, and ANIMAL is about 40 years old, so the effort is certainly admirable. But the reality is that ANIMAL is fairly rudimentary, and was in all honesty not my favorite game to convert. In effect, ANIMAL's AI amounts to creating chains of questions and answers that refine its ability to guess the animal the player has in mind. The problem with the chain approach is that chain quickly resembles a tree, and the same animal could be stored at the end of more than one "guess chain."

But, for what it is, ANIMAL is an intriguing effort at making intelligent guesses in an interactive format, so let's plow into the Javascript conversion.

The Conversion


The original ANIMAL game stores its intelligence in a simple string array, A$, that we rename 'questions' in our version. We strike the typical references to the "this" object and a host for the game console. Unlike last week's AMAZING, ANIMAL is very input-intensive, meaning we'll resort to our state-tracking mechanism for input callbacks - with LOTS of states! As noted, the intelligence is built by storing a chain of yes/no questions hopefully leading to an answer for each new animal.

ANIMAL's data structure is unique. Each string in the array is a different *kind* of data; it can be a question ("\Q" prefix), an answer ("\A" prefix , or a Y/N response ("\\Y" or "\\N" prefix) to a previous question. In the initial array, the first item contains the number of strings in the array, which we preserve even though javascript supports a direct length parameter for its arrays. The next item is a question, "DOES IT SWIM", followed by two possible answers and the array indices to follow based on the user's answer. The intelligence is a matter of building more and more strings with this structure, creating a moderately complicated chain of information used to create the illusion of ANIMAL's intelligence.

ANIMAL takes advantage of a unique feature of BASIC - the ability to encode arbitrary numeric or string data directly within a program. The "DATA" statement allows for a comma-delimited list of information that can be read at run time into program variables via the READ statement. Javascript has no such analog; but fortunately, the basic string array should serve as a great substitute. The questions array gets initialized with the number of elements, plus the first question it can ask, along with two possible answers.

function Animal(gameConsole){

     var ref= this;
  var console=gameConsole;
  var questions = [ "4","\\QDOES IT SWIM\\Y2\\N3\\","\\AFISH","\\ABIRD" ];
  var k=1;
  var state=0;
  var display = [];
  var state=0;
  var newAnimal;
  var newDifferentiatingQuestion;
  var newDifferentiatingAnswer;
  
  this.states = { GETTING_QUESTION_RESPONSE: 1,
                  GETTING_GUESS_RESPONSE: 2,
    GETTING_DIFFERENTIATING_QUESTION: 3,
    GETTING_DIFFERENTIATING_ANSWER: 4,
    GETTING_NEW_ANIMAL: 5,
    RESTARTING: 6};
      
  this.Intro = function(){
   console.writeLine("ANIMAL");
   console.writeLine("CREATIVE COMPUTING  MORRISTOWN, NEW JERSEY");
   console.writeLine("Original program by Arthur Luehrmann, Nathan Teichholtz, and Steve North");
   console.writeLine("Javascript conversion by David Whitney");
   console.writeLine("");
   console.writeLine("PLAY 'GUESS THE ANIMAL'");
   console.writeLine("THINK OF AN ANIMAL AND THE COMPUTER WILL TRY TO GUESS IT.");
   console.writeLine("");
   };

}
Our Javascript version identifies six different states - getting a list, getting a new animal, getting a differentiating question and its answer against the current animal, and getting the response for the computer's guess, That state table then gets mapped into the CommandDispatcher:

  function CommandDispatcher(command){

  switch(state){
   case ref.states.RESTARTING: 
    setTimeout(ref.ListOrResponse(command),250);
    break;
   case ref.states.GETTING_QUESTION_RESPONSE: 
    setTimeout(ref.GetQuestionResponse(command),250);
       break;
   case ref.states.GETTING_GUESS_RESPONSE: 
    setTimeout(ref.GetGuessResponse(command),250);
       break;
   case ref.states.GETTING_DIFFERENTIATING_QUESTION: 
       newDifferentiatingQuestion = command;
    setTimeout(ref.GetGuessDifferentiator(command),250);
    break;
   case ref.states.GETTING_DIFFERENTIATING_ANSWER:
    newDifferentiatingAnswer = command;
    setTimeout(ref.GetGuessDifferentiatingAnswer(command),250);
    break;
   case ref.states.GETTING_NEW_ANIMAL:
       newAnimal = command;
    setTimeout(ref.GetNewAnimal(command),250);
    break;
  }
   
  };

The handlers for most of these dispatchers is fairly simple, amounting to displaying a message, changing state, and then invoking another read routine. Play simply displays the intro and starts the game process; StartMain() is the top of the loop the program will traverse. ListOrResponse simple handles the user's response to the "Are you thinking of an animal" query, affording the user a chance to see the animals the computer "knows" about. AskQuestion displays the question at the current question index, held in the "k" variable, and then getting the user's response:

  this.Play = function(){
   ref.Intro();
   ref.StartMain();
       };
 
  this.StartMain = function(){
    state = ref.states.RESTARTING;
    console.writeLine("ARE YOU THINKING OF AN ANIMAL?");
    console.readLine(CommandDispatcher);
   };
     
  this.ListOrResponse= function(response){
    if (response=="Y"){
        ref.AskQuestion();
    } else if (response=="LIST") {
         ref.ListKnownAnimals();
        ref.StartMain();
    }
        };
  
          
  this.AskQuestion = function(){
    var string = questions[k];
    display = string.split("\\");
    console.write(display[1].substring(1,display[1].length)+"? ");
    state = ref.states.GETTING_QUESTION_RESPONSE;
    console.readLine(CommandDispatcher);
                       };
        
        
  this.GetQuestionResponse = function(response){
            if (response!="Y" && response!="N"){
           ref.AskQuestion();
     } else {
         if (response==display[2].substring(0,1)){
         k = parseInt(display[2].substring(1,display[2].length));
     } else if (response==display[3].substring(0,1)){
         k = parseInt(display[3].substring(1,display[3].length));
               }
        }
            ref.ShowGuess();
    };

"GetQuestionResponse" deserves some attention. "k" holds the current index into the question "database." The answers to the questions, which are always either "Y" or "N", also hold a "pointer" to the *next* index in the question list that should be traversed based on the user's input. That means we must parse out the integer following the Y or the N in each possible answer.

"ShowGuess" displays the computer's current guess. If "ANIMAL" gets to a point in its chain that it is ready to display an answer, indicated by the current question being prefixed with a "\A", it displays the answer and asks the user if the computer is correct. The program changes state to receive a new animal from the user, and a question that differentiates the new animal from it's guess:

  this.ShowGuess = function() {
    if (questions[k].length==0){
        console.writeLine("I have no more questions. I give up.");
    } else if (questions[k].substring(0,2)=="\\Q"){
        ref.AskQuestion();
    } else {
        console.write("IS IT A " + questions[k].substring(2,questions[k].length) + "? ");
        state = ref.states.GETTING_GUESS_RESPONSE;
           console.readLine(CommandDispatcher);
    }
    };
 
  this.GetGuessResponse = function(response){
     if (response=="Y" || response=="y"){
         console.writeLine("Great! Try another!");
         ref.StartMain(); //placeholder to restart
     } else {
         console.writeLine("WHAT ANIMAL WERE YOU THINKING OF? ");
              state = ref.states.GETTING_NEW_ANIMAL;
         console.readLine(CommandDispatcher);
     }
     };
 
  this.GetGuessDifferentiator= function(){
           console.writeLine("FOR A/AN " + newAnimal +", THE ANSWER TO THIS QUESTION WOULD BE WHAT?");
      state = ref.states.GETTING_DIFFERENTIATING_ANSWER;
       console.readLine(CommandDispatcher);
      };

When the user supplies a question that differentiates the animal from the computer's guess, the computer also has to track how that question applies to the animal guessed; that's captured by GetGuessDifferentiatingAnswer. GetNewAnimal simply displays a prompt, changes state, and calls the input handler. AddNewAnimal provides the biggest part of the program's logic, representing the core of how the question[] array is structured with its pointer-oriented list structure, sometimes moving answers within the list, and adding the "intelligence" about the newest animal near the end. ONce done, it merely restarts the game via a call to StartMain(). Lastly, ListKNownAnimals simply lists the animals the program already knows about by traversing the question array, looking for strings prefixed with "\A" indicating an answer.

  this.AddNewAnimal= function(){
    var oppositeAnswer="Y";
    if (newDifferentiatingAnswer=="Y")
     oppositeAnswer="N";
       
    var z1=parseInt(questions[0]);
    questions[0]=new String(z1+2);
    questions[z1]=questions[k];
    questions[z1+1]="\\A" + newAnimal;
    questions[k] = "\\Q" + newDifferentiatingQuestion + 
                          "\\" +  newDifferentiatingAnswer + new String(z1+1) + 
            "\\" +  oppositeAnswer +  new String(z1) + "\\";
    ref.StartMain(); 
   };
               
  this.ListKnownAnimals = function(){
         var x=0;
         console.writeLine("ANIMALS I ALREADY KNOW:");
            for (var i=1; i < questions.length;i++){
      if (questions[i].substring(0,2)=="\\A"){
                  var current  = questions[i].split("\\");
                   console.write(current[1].substring(1,current[1].length) + " ");
      }
         }
         console.writeLine("");
    };

And so we've finished our THIRD game conversion from "101 BASIC COMPUTER GAMES." I hope you enjoy these games, and will keep coming back each week. Blessings, David

No comments:

Post a Comment