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







No comments:

Post a Comment