Showing posts with label games. Show all posts
Showing posts with label games. Show all posts

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

Monday, June 22, 2015

Watch and be Amazed!!! - Game 2

Okay, okay, cornball title. But you could hardly blame me for taking such an approach to this week's "101 BASIC Computer Games" conversion of Game 2: "Amazing." The fact that I decoded this particular chunk of BASIC is, well, a little amazing all by itself (groan). :). Be sure to follow the original BASIC source for AMAZING at Atari Archives. As always, I invite you to join the blog, leave a comment, and have a great time as we look at these great old games!

Download the full set of files for this week's port at our SourceForge repository!!

Perhaps more than anything else, "Amazing" demonstrates for us the truly unpleasant evil of abused GOTO statements in our old BASIC language source. Decoding the design intent of the original author from the source is manifestly difficult because so many switches in logic are reflected only in jumps to line numbers, that lead to more jumps to more line numbers, and then jump back near the beginning, to still more line numbers - and in many cases, to execute blocks of freqently repeated code.

The unfortuante aspect of this "death by 1,000 GOTO's" is that a rather elegant algorithm for constructing this electronic maze is buried amid the GOTO's. I remember studying this program extensively when I first bought this book, finding myself fascinated at the way the program was described as one *guaranteeing* only one path through - yet never understanding why. These undescriptive variables and chained GOTO's were part of the reason why I couldn't quite keep it straight - to say nothing of the single-letter variable names that didn't seem to remotely reflect their purpose.

Amazingly simple


"Amazing" models a maze as a simple grid of "cells" in the height and width specified by the user (see Figure 1). Building the maze amounts to knocking out walls by carving paths through the maze.
A simple 5x5 maze grid

Carving out paths


To start, each cell in the grid has four walls - north, east, south, and west. To carve out a path through the maze, it follows a simple algorithm:
1. Place a "visitor" in the maze - initially this is the "start" location. Mark the cell as having been "visited."
2. The "visitor" moves randomly by one cell in any direction - up, down, left or right - subject to the following rules
     a. The visitor cannot go beyond the natural boundaries of the maze.
     b. The visitor cannot go to a cell that's already been visited.
3. Remove the wall between where the visitor was and where he now stands.
4. If all cells have been visited, the maze is complete.
5. Repeat the process at step 2 unless the visitor has hit a dead end per the movement rules.
6. Retart the process, re-starting the visitor at a cell already visited on a previous path.

Amazing tracks "visited" cells separately from the maze's "walls." While each cell has four physical walls, those walls become two logical walls in the program - east and south (Figure 2).
Figure 2 - Cell walls
Each cell's north wall is actually modeled as the south wall of the cell directly to its north; each west wall is modeled as the east wall of the cell directly west.  Amazing holds the wall information in a two-dimensional array named "V", and the visitation list in an an array named "W" - try decoding those two letters from four-decade-old dot-matrix print with 50-year-old eyes through a bifocal!!

Amazing - Javascript style


Let's start the source for our Javascript version. The visitor tracking variables R and S become currentColumn and currentRow, respectively; H and V become width and height. "C," holding the cell number, becomes cellCount, and a curious ping-pong of "Z" and "Q" used to control when and if the exit has been created are replaced with a simple boolen "exitPlaced" variable.

function Amazing(gameConsole){

    var ref=this;
    var width, height;  // maps to W, H in original
    var wVisited=[];   // maps to "w" array in original; will become 2d when initialized [][]
    var vWalls=[];     // maps to "V" array in original
    var cellCount=1;
    var currentRow, currentColumn;
    var QControl=0;
    var console = gameConsole;
    var	ZControl=true; // ZControl indicates whether an exit on the bottom row has been defined
	               // This allows us to go "down" on the bottom row *one* time - to define an exit.
    var exitPlaced = false; 
    var visitedCellList= [];	
	this.Play = function(){
		console.writeLine("AMAZING PROGRAM");
		console.writeLine("CREATIVE COMPUTING - MORRISTOWN, NEW JERSEY");
		console.writeLine("Original program credit: Jack Hauber of Windsor, Connecticut");
		console.writeLine("Javascript port: David Whitney, Oklahoma City, OK");
		ref.GetDimensions();
	}

How it works



Figure 3 illustrates the first two theoretical paths through a sample 5x5 maze. The yellow arrows represent a "first" path traversal, entering at the maze starting point (Start #1, picked randomly along the first row), followed by a path carving sequence of right-down-right-up before dead-ending. Each cell visited in the first path is slightly shaded to indicate it having been visited. Following the dead-end of the first path, the algorithm repeats by randomly selecting a new starting position (Start #2) for the visitor somewhere along a non dead-end cell from any previously constructed path, moving again in once-cell steps, stopping when a dead end is reached (End #2).


The process of tracing paths by always starting the visitor in a location previously visited means that we're already expanding a previously defined path. Continuing that strategy eventually will extend, as noted, to the bottom row, and an exit. The code allows such a choice - going "down" from a cell on the bottom row - one time. Once the "exit" has been defined, the perpetual extension of the remaining paths simply fill the rest of the maze until all cells have been visited. That's how the code guarantees a single exit path from start to finish.

What makes this simple algorithm so hard to "see" amid the original BASIC source is, in addition to the GOTO's, the repeated "chunks" of code that are really doing nothing more than bounds checking to determine which "chunk" of options are possible for the visitor at a given point. The original source wants to know ahead of time if it's going to generate a choice, for example, between left, up, or down (line 350), up, right, or down (line 580), and so on. Doing this, however, requires pieces of the bounds checking code to be repeated.

Rather than try to reproduce this kind of logic, I opted to redesign it. I wrapped the bounds checking code into four simply named routines - canMove{Up/Down/Left/Right}(). The visitor is then allowed to make a random choice from among any of the "available" directions. If no directions are available, the visitor is dead-ended, and must restart his journey on a new location (cell) from among those previously visited.

	function canMoveLeft(){
	    if (currentColumn==0)
			return false;
		else
			return (wVisited[currentColumn-1][currentRow]==0);
	};
	
	function canMoveRight(){
		if (currentColumn==width-1)
			return false;
		else
			return (wVisited[currentColumn+1][currentRow]==0);
	};
	
	function canMoveDown(){
		if (currentRow==height-1){
			if (exitPlaced){
			    QControl =1;
				return false;
			} else {
				return true;
			}
		}
		else
			return (wVisited[currentColumn][currentRow+1]==0);
	};
	
	function canMoveUp(){
		if (currentRow==0)
			return false;
		else
			return (wVisited[currentColumn][currentRow-1]==0);
	};
	// Replaces BASIC source lines 820-850
 	function moveUp(){
		wVisited[currentColumn][currentRow-1]=cellCount; 
		vWalls[currentColumn][currentRow-1]=1;
		visitedCellList.push( {column: currentColumn, row: currentRow-1} );
		currentRow--;
		cellCount++;
	}
	
	// Replaces BASIC source  lines 910-955
	function moveDown(){
		if (currentRow!=height-1){
		//if (QControl==0){
			wVisited[currentColumn][currentRow+1]=cellCount;
			visitedCellList.push( {column: currentColumn, row: currentRow+1} );
			cellCount++;
			if (vWalls[currentColumn][currentRow]==0){
				vWalls[currentColumn][currentRow]=1;
			} else {
				vWalls[currentColumn][currentRow]=3;
			}
			currentRow++;
		} else {
			ZControl = false; // exit found, can't go down on bottom row now
			exitPlaced=true;
			//console.write("Exit placed.");
			if (vWalls[currentColumn][currentRow]==0){
				vWalls[currentColumn][currentRow]=1;
				currentRow=0;
				currentColumn=0;
			} else {
				vWalls[currentColumn][currentRow]=3;
			}
			QControl=0;
		}
		
	}
	// BASIC lines 860-905
   	function moveRight(){
		wVisited[currentColumn+1][currentRow]=cellCount;
		visitedCellList.push( {column: currentColumn+1, row: currentRow} );
		cellCount++;
		if (vWalls[currentColumn][currentRow]==0){
			vWalls[currentColumn][currentRow]=2;
		} else {
			vWalls[currentColumn][currentRow]=3;
		}
		currentColumn++;
	}
	// BASIC lines 790-815
	function moveLeft(){
		wVisited[currentColumn-1][currentRow]=cellCount;
		visitedCellList.push( {column: currentColumn-1, row: currentRow} );
		cellCount++;
		vWalls[currentColumn-1][currentRow]=2;
		currentColumn--;
	}
Another change in the program's design I opted to implement improves the efficiency of the maze computation in a couple of ways. The original source, upon dead-ending, always moved linearly in a left-to-right, top-to-bottom fashion looking for an unvisited cell, sometimes restarting in the upper left-hand corner. I think this tended to generate mazes that were more "open" near the top, with longer walls at or near the bottom. To fix this, I added a "visitedCellList" array that gets the coordinates of each cell as it is visited. When the visitor's starting position must be assigned, a unlocked cell from this list is chosen at random, distributing the starting positions more evenly:
	function isLocked(column, row){
	    return ( 
				 (column==0 || (wVisited[column-1][row] > 0)) &&
				 (row==0 || (wVisited[column][row-1] > 0)) &&
				 (column==width-1 || (wVisited[column+1][row] > 0)) &&
				 (row==height-1 || (wVisited[column][row+1] > 0))
			   );
	}
	function findStartingLocation2(){
	
		while ((wVisited[currentColumn][currentRow]==0) || isLocked(currentColumn,currentRow)){
			var newLocationIndex = random(visitedCellList.length-1);
			currentColumn = visitedCellList[newLocationIndex].column;
			currentRow    = visitedCellList[newLocationIndex].row;
		}
	};
        
The redesign of the start position selection and movement logic allowed the generation of the entire maze to be reduced into a single loop, with the outermost loop merely checking the current cell count, and the inner loop moving the visitor so long as he isn't deadlocked along the current path. When the cell count is reached, indicating all cells have been visited, the maze is complete. For each iteration, we create a list of valid possible directions (validDirections[]), then pick a random number from among that list:
	function constructMazePaths(){
	    
		// starting at the currentColumn,currentRow, snake up/down/left/right until we 
		// run out of cells or can't go anywhere else; then find a new starting position
		// and continue
		
		while (cellCount<=width*height){
		
		    var validDirections = [];
			findStartingLocation2();

			var onCurrentPath = true;
			while (onCurrentPath){

				validDirections.length=0;

				//var onCurrentPath = (canMoveUp() || canMoveRight() || canMoveDown() || canMoveLeft());
				// Direction are mapped 1:Up, 2:Right,3:Down, 4:Left
				// Push valid current possible directions into an array, then
				// randomly select from among the valid possible values. 
				if (canMoveUp()){
					validDirections.push(1);
				}
				
				if (canMoveRight()){
					validDirections.push(2);
				}
				
				if (canMoveDown()){
					validDirections.push(3);
				}
				
				if (canMoveLeft()){
					validDirections.push(4);
				}
				
				//if validDirections is length=0, no valid values, we're locked.
				onCurrentPath = (validDirections.length>0);
				
				if (onCurrentPath){
					direction = validDirections[random(validDirections.length)-1]; 
					switch(direction){
						case 1: moveUp();
								break;
						case 2: moveRight();
								break;
						case 3: moveDown();
								break;
						case 4: moveLeft();
								break;
					}
				}
				onCurrentPath = onCurrentPath && (cellCount<=width*height);
			}
		}
	}

Rendering the maze is a simple matter of iterating through the wall matrix one row at a time, interrogating each cell to determine if an east and/or a south wall must be displayed. I preserved the original program logic for this, reserving vertical walls in one physical line, and horizontal walls on the next. This could could be condensed and allow for larger display mazes if it were reworked to use an underscore and a vertical bar (pipe) for the south and east walls.
	function renderMaze(){
		for (var j=0; j< height; j++){
			console.write("I");
			for (var i=0; i< width; i++){
				if (vWalls[i][j]<2){
					console.write("  I");
				} else {
				    console.write("   ");
				}
			}
			console.writeLine("");
			for (var i=0; i< width; i++){
				if ((vWalls[i][j]==0) || (vWalls[i][j]==2)) {
					console.write(":--");
				} else {
					console.write(":  ");
				}
			}
			console.writeLine(".");
		}
	}

The input for Amazing wants two comma-separated integers, and this is where I discovered a bug in our ConsoleWindow class. The keydown handler did not properly map the keycode for a comma, returning 188 and causing a "1/4" symbol to be displayed. A tweak to map this code to 44, for an actual comma, fixed this issue. The maze dimensions are the only input to Amazing, hence our CommandDispatcher is fairly trivial. We also provide a simple command validator to ensure numeric input, and initialize the visitation and cell wall arrays. I also added a simple utility function to provide a little syntactic sugar to random number generation:
    function CommandDispatcher(command){
		setTimeout(ref.BuildMaze(command),250);
	}
	
	this.GetDimensions = function(){
	                            console.writeLine("WHAT ARE YOUR WIDTH AND LENGTH?");
								console.readLine(CommandDispatcher);
						};


	function validArguments(param){
		
		var parms = param.split(",");
		
		if (parms.length != 2){
			return false;
		}
		
		width = parseInt(parms[0]);
		height = parseInt(parms[1]);
		
		return (!isNaN(width) || !isNaN(height))
	}

	function initArrays(){
	    for (i=0; i< width; i++){
			wVisited[i]=[];
			vWalls[i]=[];
		    for (j=0; j< height; j++){
			    wVisited[i][j]=0;
				vWalls[i][j]=0;
			}
		}
	};		
	
    function random(number){
        return Math.floor(Math.random()*number)+1;
    };

All that's left now is to wrap up these helper methods into the core routine that will take the input, carve the paths, and render the maze, and that's in the BuildMaze function off our Play() method:
	this.BuildMaze = function(command){
						if (!validArguments(command)){
						    console.writeLine("MEANINGLESS DIMENSIONS. TRY AGAIN.");
							ref.GetDimensions();
							return;
						}
						
						// valid width, height in (surprise) width, height;
						initArrays();
						
						startOpening = random(width);
						QControl=0;
						ZControl=0;
						
						// maps lines 165-190 for top line of maze
						for (var tc=1; tc<=width; tc++){
							if (tc!=startOpening)
								console.write(".--");
							else
								console.write(".  ");
						}
						console.writeLine(".");
						
						wVisited[startOpening-1][0] = cellCount;
						cellCount++;
						
						currentColumn=startOpening-1;
						currentRow=0;
						
						constructMazePaths();
						renderMaze();
						// now we start snaking through a path
					};


This was a fun and rather challenging port, because it was less about the language differences than it was decoding a design strategy buried in a "maze" of BASIC code. Here's hoping you enjoyed reading through and playing with the project as much as I did!!
Grab the code and the updated ConsoleWindow from the SourceForge repository, and please feel free to leave comments below.
Until next week!

Saturday, June 6, 2015

Nostalgia, Games, and porting BASIC to Javascript - Part One!

As my daughter finished her junior year in high school, and my son his first in college, I found myself in a nostalgia wave this last month. I thought about how things have changed in my 50 years, how quickly time passes, and amid that flow was a reminder of how much my own profession has changed. The world of computing and programming isn't what it was 30 years ago.

As the venerable Radio Shack chain came to a rather ignominous end this year, I came across more than a few ads that took me back to my teenage years when I dove into "computing" with a klunky but functional TRS-80 computer - a "Level I" version with an adapted black-and-white TV as a monitor, a 64x16 black and white upper-case-only character cell display, 4K of RAM, a minimalist version of the BASIC programming language, and a cassette tape interface for recording programs.

I typed in a few programs, started learning BASIC, and realized almost instantaneously that I was hooked.

Soon realizing that Level I version wasn't good for much in practical terms, a bit of wheedling with my mom persuaded her to "upgrade" me for my birthday to the "Level II" model, with 16K of RAM and a better version of BASIC. I started writing programs that actually did practical things, learning about sorting, organizing my programs, and getting first-hand frustrations with the vagaries of the cassette tape interface. There were few things more frustrating than spending an hour or more typing in a program, and CSAVE-ing it to tape, only to discover the save didn't "take" when you tried to CLOAD the program back later. And that happened more than once.

As I persued the bookshelves at the old B. Dalton bookstore in the mall near my home, I came across a tantalizing book title: "101 BASIC Computer Games" by Dr. David Ahl of "Creative Computing" magazine. I already knew about "Creative Computing." It was one of the first big, important monthly computing magazines of the era. I had saved enough for a year's subscription and found it supremely fascinating. I even sent them an unsolicited text about artificial intelligence (which they politely rejected). And I desperately wished I'd had enough money to buy the Heathkit dot-matrix line printer kit described in an article in one issue - but I couldn't quite wheedle that out of my parents. But this "101 BASIC Computer Games" lit me up like a Christmas tree.

I thumbed through the book and found page after page of BASIC source listings for games from Football, Tennis, Acey-Deucy, to banners and calendars, to the penultimate - a game called "Super Star Trek" - all ported from numerous divers mainframe environments into a relatively neutral version of BASIC similar to the one that had become pervasive on the first pre-PC-clone generation of personal computers. All you needed was good eyesight and the patience to type in the source into whatever personal computer you might have. If your dialect of BASIC was a little different, you were on your own to get the porting right.

I dove in like a kid in the proverbial candy store.

Some 35 years later, I still have that big, yellow book, and fortunately it's still in very good condition. The wave of nostalgia combined with the discovery of that great old book led me to an idea - how much fun it might be to update those programs into a contemporary environment in a contemporary language. Not because there's some great demand for it, but just for the fun and homage of what that book meant to the fledgling home computing industry, and the joyous memories they held for me those years ago.

I first started to port some of the games to C#, but found that some folks had already started an effort to port "Super Star Trek" to C back in 2008, so there was little point in retreading old ground. But some posts I saw on that project host page offered an intriguing idea - "What about porting this to Javascript!?"

What about that idea, indeed!!

In much the same vein as erstwhile blogger Julie Powell undertook to cook her way through Julia Child's "Mastering the Art of French Cooking," I'm going to try and port most - if not all - those great "101 BASIC Computer Games" into Javascript. Now, let's be honest - is there some huge demand for this? Surely not. I doubt there is much practical about such an undertaking aside from the mental exercise and novelty. But I also suspect a few other long-timers such as myself might find that novelty worth sharing, and that sharing starts here. I'll aim to post about a game a week, maybe more, maybe less, depending on how much spare time I have and how long any one game takes to port.

I've startaed up a SourceForge site for these proejcts, just in case someone else with a similar mania finds these efforts worth exploring. Mind you, these will be fairly straightforward conversions - plain Javascript, no jQuery - wherever possible. And I won't promise to write the best Javascript out there - I'm sure there are plenty of sharp Javascript coders who can tell me how I can write stuff "better," and that only helps me. This project is about my own odd kind of geeky fun.

We'll kick off this effort with a bit of a "tease" of what's ahead - a project not even in the "101 BASIC Computer Games" book, but one we can't proceed without.

A Minor Requirement


The first thing I realized when I opted to embark on this minor, strange project was the fact that these great, old Ahl games depended on on simple thing: a character-based console environment. For those too young to know exactly what that means, let's embark on a brief history lesson.

Back in the days before LCD and LED high-definition screens and gigabytes of memory, computers rendered their output on simple video displays that rendered only fixed-width symbols and alphanumeric characters along with a few limited block-style graphics characters within a fixed grid, often 80 columns wide by 24 lines deep. While advanced for their time, these video consoles were little more than next-generation versions of hardcopy paper teletype terminals that did one thing: print lines of text on paper.

Teletype consoles and their "advanced" video console descendants offered little in the way of variety. Line widths could typically be varied from 40 to 132 columns, and video consoles could sometimes be adjusted to display varying numbers of lines. Most consoles were white characters on a black background; later versions rendered text in green as it was deemed easier on the eyes for extended viewing.

Early "personal" computers inherited this character-cell orientation, and the migration of BASIC from the mainframe platforms of that era formed the basis for Ahl's classic compilation. Despite the ubiquity of BASIC, the subtle differences in implementation among the multiple platforms - diverse among the mainframes, then diverse again among personal computers - made the migration of these great games into a generally consumable format all the more amazing.

With the history lesson in hand, we now know that before we can attempt to migrate these games from text-oriented BASIC into Javascript in an HTML world, we need a console output device on which to render them; hence, the first project: A Javascript text ConsoleWindow.

Project Zero - A Javascript Text Console

Attacking the design

A text console needs specific visual properties. It needs to be rendered in a fixed-width (monospaced) font; the particular font face isn't terribly important. We want to declare a console of *exactly* the desired with and height. We'll settle for a default of white-on-black for text, and think of color support perhaps in a second version (hint, hint). When printing to the display, we need to fill from top to bottom, and scroll the top out as the bottom fills up. Virtually all of these issues can be controlled via HTML styling. We control the console's content via the innerHTML property of the element we choose to represent the console.

We'll generate the console as a DIV element, and create an inline style that defines most of these properties. For the style, we specify a "font-family" value of "monospace", and let the browser pick the one it wants. If text is written to the end of a line, it should wrap to the next line; hence, we set a "word-wrap" value of "break-word". We want no scrollbars or resizing on the console, so we'll set "overflow: hidden", and get our white-on-black text with "color: white" and "background-color: black". One thing we can't do statically is establish the size of the DIV, because we won't know that until runtime.

To avoid requiring the user to specify such mundane things as explicit point, font, and pixel sizes, we'll spin up a simple enumeration that defines five possible "canned" console sizes, with fonts ranging from 10 to 18 pixels in two-pixel increments.

Starting the code

We'll make the Console a simple Javascript object, initialized with its required columns, rows, and size. We'll strike an internal reference for the object as we'll implement private and public methods to manage our console, as well as our size enumeration:


function Console(columns, rows, size){

   var ref = this;
   this.columns=columns;
   this.rows=rows;

   this.SizeInfo = {
Tiny  : { value: 1, cellHeight: "10px", cellWidth: "6px", fontSize: "10px"},
Small : { value: 2, cellHeight: "12px", cellWidth: "8px", fontSize: "12px"},
Normal: { value: 3, cellHeight: "14px", cellWidth: "10px", fontSize: "14px"},
Big   : { value: 4, cellHeight: "16px", cellWidth: "12px", fontSize: "16px"},
Large : { value: 5, cellHeight: "18px", cellWidth: "14px", fontSize: "18px"}
};

}


Sizing the console


How do we create a console of exactly the size required? Measuring browser text can be tricky because not every font is available in every system, and most situations involve characters of varying widths. For our purposes, a console with a monospaced font means if we can measure one character, regardless of the *actual* font, we know the size of *every* character, and thus should be able to size our div precisely in both dimensions.

To do this, we'll create a SPAN with a font-family style of "monospace", and the font-size defined by the 'size' parameter to the constructor. We fill the innerHTML property with an aribtrary character, and once the SPAN is added to the document body and rendered, it will have a bounding rectangle we can query for height and width via getBoundingClientRect(), then remove from the document. Moreover,if we do this in the constructor, the "rendering" will be in memory only, never to be seen by the user.

var testSpan = document.createElement("SPAN");
testSpan.style.fontFamily="monospace";
testSpan.style.fontSize=this.size.fontSize;
testSpan.innerHTML='X';
document.body.appendChild(testSpan);
cellRect = testSpan.getBoundingClientRect();
document.body.removeChild(testSpan);
  
// Extrapolate the size of a single monospaced letter to a full console
var consoleHeight = cellRect.height * rows + 'px';
var consoleWidth  = cellRect.width  * columns + 'px'; 

With all parameters for the console at hand, we can now construct the STYLE attribute programmatically, then attach it to the head of the document:

var consoleCSS = document.createElement("STYLE");
consoleCSS.type = "text/css";
consoleCSS.innerHTML = '.console { overflow: hidden; background-color: black; word-wrap: break-word; ' +
' color: white; font-family: monospace; font-size: ' + this.size.fontSize + '; '+ 
    ' display: inline-block; min-height: ' + consoleHeight + '; max-height: ' + consoleHeight + '; ' +
' min-width: ' + consoleWidth +'; max-width: ' + consoleWidth +'; }; ';

// Append the console style sheet.
document.getElementsByTagName("head")[0].appendChild(consoleCSS);

Now, all that matters is adding the console itself, with an appropriate class name to match our style:

var consoleDiv = document.createElement("DIV");
consoleDiv.className='console';
document.body.appendChild(consoleDiv);

The baseline console is now in place, but with no methods to write to it. Our design is focused on simply appending text to the DIV via the innerHTML property, and exposing two 'write' methods; one that appends a "carriage return/line feed," and one that doesn't. We can implement this by simply appending an HTML tag to force a break where we want the CR/LF to appear. Also, we have to ensure we write actual spaces to our console, and to do that, we have to replace any literal spaces in the input with the HTML no-break-space encoding. Lastly, we must make sure that whatever we write forces the top to scroll out if the screen is full, which we can accomplish by setting the DIV's scrollTop property to its scrollHeight after changing the innerHTML. That gives us our two write methods:

// Write with no "CR/LF"
this.write = function(text){
consoleDiv.innerHTML += text.replace(" "," ");
consoleDiv.scrollTop = this.theAltConsole.scrollHeight;
}

// Write with CR/LF as a break tag - br
this.writeLine = function(text){
  this.write(text+ "") ;
}


A 'clear' method simply amounts to erasing the content of the innerHTML property:

this.clear = function(){
consoleDiv.innerHTML =''; //blanks;
consoleDiv.scrollTop = consoleDiv.scrollHeight;
    }

The next requirement is the trickiest: Data input. At various times, all the games will need to receive input from the user. In original BASIC, an "INPUT" statement allowed a program to display a message with a trailing cursor, and then halt until the user typed something and hit [ENTER]. BASIC would transfer what the user entered into a variable specified at the end of the INPUT statement.

Simply put, there's just no way of duplicating this in Javascript. Most fundamentally, the very notion of trying to force Javascript to "block" on input sends shudders down our backsides, because neither browsers nor the Javascript engine itself just were designed to operate that way. Block the Javascript thread, and you'll likely end up locking your browser.

All this means we have to adapt our model to fit Javascript's world, and that means we need to trap keystrokes with an event handler.

For this purpose, our event handler must trap keystrokes, accumulate them into a string, and when the input is complete, return that string to the caller. The best place to capture keystrokes from the console is by placing our event handler with the 'onkeydown' event of the document. We also need to capture only "printable" or "displayable" characters. Lastly, once the user is done, we've got to send that data back to the caller; however, our console won't know anything about it's caller, so the caller will have to send that mechanism to us. That's known as a *callback function*.

We'll keep our keystroke handler simple. With two exeptions, we're going to ignore any non-displayable characters - those having a keyCode lower than 32 - a space. In general, as each displayable character is pressed, we'll add it to an internal string, and continue to do so until the user hits the [ENTER] key, which becomes one of our non-displayable exceptions (keyCode 13). We also must allow for the user to [BACKSPACE] over mistakes (keyCode 8), which is our other exception. Here's the entire input routine, and we'll dissect things as we go along.

this.readLine = function(recipientCallback){
  ref.inputString="";
ref.inputLocked=true;
ref.write("_");
document.body.onkeydown= function(){
      var key = event.keyCode || event.charCode;
if (key < 32){
         if (key==8){ //backspace
             event.preventDefault();
if (ref.inputString.length>0){
                ref.inputString = ref.inputString.substring(0,ref.inputString.length-1);
                inputBackspace();
}
         } else if (key==13) { //enter, terminate
              ref.inputLocked=false;
event.preventDefault();
document.body.onkeydown=null;
consoleDiv.innerHTML = consoleDiv.innerHTML.substring(0,consoleDiv.innerHTML.length-1);
if(recipientCallback){
                 recipientCallback(ref.inputString);
}
ref.writeLine("");
         }
} else {
         event.preventDefault();
         var newChar = String.fromCharCode(key);
         consoleDiv.innerHTML =              consoleDiv.innerHTML.substring(0,consoleDiv.innerHTML.length-1);
         ref.write(newChar + "_");
         ref.inputString += newChar;
}
}
}

We initialize the input string to blank when we start the function, and render the "cursor" to show the user we're awaiting input. We then initialize the key handler as an anonymous function, identifying the character pressed by inspecting the keyCode or charCode properties of the event object for each firing of the onkeydown event.

Appending displayable keystrokes - keyCode >=32 - is simple: We simply call our 'write' method to display it, and concatenate the new character to our input string. The [ENTER] key - keyCode 13 - signals the end of input, where we disconnect the handler by setting onkeydown=null, remove the cursor, and send the string back to the caller via its callback. The backspace key, however, is trickier.

Backspacing requires several steps. We have to truncate the last character of the input from the displayed innerHTML *and* our internal input string. We must be aware that in innerHTML, hard spaces will be stored as no-break spaces - nbsp; - so just deleting the last character on a backspace won't work. We *don't* have to make this check for the internal input string, because we add the uncoded version. With all this in mind, we wrap backspace handling into a private function:

function inputBackspace (){
     // copy innerHTML locally for simplicity
   var txt = consoleDiv.innerHTML;
   txt = txt.substring(0,txt.length-1); // trim the cursor

     // check for backspacing an encoded space
   if (txt.substring(txt.length-6,txt.length)==" "){
       txt = txt.substring(0,txt.length-6);
   } else {
txt = txt.substring(0,txt.length-1);
   }
     // replace the cursor
   txt += "_";
   consoleDiv.innerHTML = txt;
}

Put it all together, and that should represent the entirety of our Javascript ConsoleWindow. We can test it by adding some buttons to a regular page that will instantiate a window, and exercise the methods we've written. I've posted the console as a standalone .js file and as a single HTML file at this SourceForge repository with the code included that also has some test buttons that will let you exercise the console, firing up an 80x24 window in a default "size 3" variety. Here's a sample of the HTML file in action on IE11:



Play with the code, look at it, tweak it, let me know how you've enhanced it. While you do that, here are some obvious limitations:
  1. Long game sessions that never clear the console can result in large content blocks in the console's innerHTML. That might result in memory issues that affect performance.
  2. This console isn't *addressable*. We've mimicked a teletype/green-screen with straight line dumps that scroll up and off the screen. An *addressable* screen would allow the specification of a particular x-y cell coordinate at which to print text; this innerHTML implementation makes that essentially impossible. A later version, which would entail a significant rewrite, could include this important and useful feature (hint, hint? A keen observer might note this was called console_v1.html")
This will be the baseline ConsoleWindow used for all of these BASIC game ports. And, we'll give this one a shot with our very first game...next week.

With all great appreciation to the great work of Dr. Ahl and the old "Creative Computing" team.

Enjoy!

-David