Chapter 9

Games


CONTENTS

Web-based games are a realm in which JavaScript shines. Although you don't need JavaScript to write Web-based games in which the user interacts with the computer, as a rule games written without JavaScript are much slower and thus less fun.

The scenario

Most people know the game of hangman from grade school. The first player chooses a word and states how many letters it contains; the second player tries to guess the word. The second player picks a letter, and if that letter is present in the word, the first player identifies where the letter appears in the word. If the letter isn't present in the word, the first player draws a stick figure of a person being hanged, one body part at a time. As I recall, you draw the head, followed by the body, the two arms, and the two legs. If the second player makes six wrong guesses, the stick figure is complete, and the first player has stumped the second player.

The requirements

You will create a Web page that allows the user to play hangman against the computer. The computer, through the Web page, will choose the word, and the user will attempt to guess the word.

When the word is chosen, a string of underlines separated by spaces is displayed, one underline per letter. For example, if the word "syzygy" is chosen, this is displayed:

_ _ _ _ _ _

The hangman's gallows is also displayed:

+---+
|/  |
|   0
|
|
|
+======

When the user guesses a letter, the letter is displayed if it is in the word. For example, if the word is "syzygy" and the user guesses "y," the display is updated like this:

_ y _ y _ y

If, on the other hand, the letter is not in the word, another character of the hanged man is added to the gallows. For example, if the word is "syzygy" and the user guesses "e," the gallows display is updated like this:

+---+
|/  |
|   @
|
|
|
+======

On incorrect guesses, the gallows display acquires, in turn, the head, the body, the left arm, the right arm, the left leg, and the right leg. The final gallows display, if the user is stumped, looks like this:

+---+
|/  |
|   @
|  -|-
|  / \
|
+======

The user should not be penalized for guessing the same letter twice, and if possible, should not be permitted to guess the same letter twice. The user is given a score, based on how many letters he or she guessed wrong. A running average (the total scores divided by the total games played) should be displayed.

The solution (without JavaScript)

The conventional solution (non-JavaScript) to writing a game page is to first display a page with the rules and a button to indicate that the user is ready to start playing the game. When the user presses the button, a CGI program generates the first HTML page that the user will play on. The first word is chosen and is contained in an INPUT HIDDEN element. The CGI program writes the HTML to display the underlines and the gallows, and the page includes an INPUT element that the user can use to choose the letter. The user selects a letter, and the form data is sent back to the CGI program. The CGI program then evaluates the new letter, creates a new page with the letter showing or another character in the hanged man. The flow looks like this:

User sees welcome page
User presses button to start
--------------------------------------> CGI program activated
New page is displayed <----------------
User selects letter ------------------> CGI program evaluates letter
New page is displayed <----------------

And on and on it goes, until the user guesses the word or is stumped.

The heart of this solution is the CGI program that creates each new page. The HTML that you write is almost an afterthought. It simply presents the rules and provides a FORM element with a SUBMIT button, like the code in Listing 9.1, which produces the form shown in Figure 9.1.

Figure 9.1 : Welcome page.


Listing 9.1: Welcome page
<HTML>
    <HEAD>
        <TITLE>Hangman</TITLE>
    </HEAD>
    <BODY>
        <P>
            Welcome to Hangman! The object of this game is to guess a
            word without getting hanged! You can get five letters wrong
            - and then you're hanged!
        </P>
        <FORM ACTION="http://someserver.somewhere/cgi-bin/hangman"
            METHOD="get">
            <INPUT TYPE="submit" VALUE="Play Hangman">
        </FORM>
    </BODY>

</HTML>


The solution (with JavaScript)

The big problem with the non-JavaScript solution is the enormous expense in bandwidth. With every guess, a new HTML page is downloaded from the server. That's a lot of traffic!

From the user's perspective, this translates to a slow game. It's okay for the user to deliberate about his or her next move, but the computer is expected to respond instantly! Instead, the user gets to watch the status bar display the number of seconds until the next page is downloaded.

A game of any length-where lots of screens are being loaded-will also eat your user's history. If the user had visited a page before playing the game and wanted to go back to it, the user would be out of luck unless he or she had bookmarked that page.

With JavaScript, the user can get the desired instant feedback.

First, let's lay out the screen. You need a frame that holds the current results-the gallows and the underlines. Another frame holding the running average would be nice. And you need a frame to get the letters from the user. A simple frame document like this will suffice:

<HTML>
    <HEAD>
        <TITLE>Hangman II</TITLE>
    </HEAD>
    <FRAMESET ROWS="*,*">
        <FRAMESET COLS="*,*">
            <FRAME SRC="empty.htm" NAME="scoreboard">
            <FRAME SRC="empty.htm" NAME="playingfield">
        </FRAMESET>
        <FRAME SRC="hangctrl.htm" NAME="controls">
    </FRAMESET>
</HTML>

The empty.htm file used in the scoreboard and playingfield frames is a simple placeholder file that is repeatedly overwritten as the game progresses. The hangctrl.htm file provides the user controls and the JavaScript code to update the scoreboard and playingfield frames.

Let's start with the score frame. This frame simply displays the running average. To do that, you need data-the current sum of scores and the number of games played. Because the user may play hangman, go visit some other pages, and then come back and play another game of hangman, let's store the data in a cookie. This is a data structure that your JavaScript pages can use to store data on the user's disk and retrieve it later. While you can store much more, you only need to store a variable name and a value. The function that initializes the scoreboard frame will try to load the total points and total words from cookies. If they aren't available, such as when a user plays the games for the first time, the function will create cookies with values of 0 for the total points and total words. Normally, the value displayed will be the total points divided by the total words played, but with initial values of 0, that doesn't work. Division by zero is an illegal operation, so the function to initialize the scoreboard has to take that into account. Finally, the function will display the score on the scoreboard frame:

var totalPoints;
var totalWords;
var runningAverage;
function initializeScoreBoard()
    {
    var tP = getCookie("HMPoints");
    if (tP == null)
        {
        neverPlayed();
        }
    else
        {
        var tW = getCookie("HMCount");
        if (tW == null)
            {
            neverPlayed();
            }
        else
            {
            totalPoints = eval(tP);
            totalWords = eval(tW);
            }
        }
    if (totalWords == Ø)
        {
        runningAverage = Ø;
        }
    else
        {
        runningAverage = Math.round(1ØØ * (totalPoints / totalWords));
        }
    displayScoreBoard();
    }

The function getCookie() tries to find a cookie with the specified name; it returns the special value null if no such cookie exists. Otherwise, it returns the specified cookie's value as a string:

function getCookie(s)
    {
    var target = s + "=";
    var targetLength = target.length;
    var index = Ø;
    while (1) // search while 1 is nonzero, i.e., forever
        {
        var offset = index + targetLength;
        if (document.cookie.substring(index, offset) == target)
            {
            var tail = document.cookie.indexOf(";", offset);
            if (tail == -1)
                {
                tail = document.cookie.length;
                }
            return unescape(document.cookie.substring(offset, tail));
            }
        index = 1 + document.cookie.indexOf(" ", index);
        if (index == Ø || index >= document.cookie.length)
            {
            return null;
            }
        }
    return null;
    }

The getCookie() function searches the document's cookie property for the specified name followed by an equal sign. If that string is found, the value is the substring that follows the equal sign and ends with a semicolon. If there is no semicolon, the rest of the cookie property string is returned. In either case, the value is passed to the unescape() function to translate escaped characters back into normal text. If the name= string is not found, getCookie() scans ahead to the next space and searches again. If the end of the document's cookie property is reached, the cookie is not there and a null is returned.

The neverPlayed() function creates new total points and total words cookies and sets them to zero; it also sets the variables totalPoints and totalWords to zero:

function neverPlayed()
    {
    totalPoints = Ø;
    totalWords = Ø;
    createCookie("HMPoints", Ø);
    createCookie("HMCount", Ø);
    }

The createCookie() function is very simple; it runs the value parameter through the escape() function to translate special characters like spaces and semicolons into their hexadecimal values. It then creates the cookie:

function createCookie(name, value)
    {
    document.cookie = name + "=" + escape(value);
    }

Finally, initializeScoreBoard() calls displayScoreBoard() to fill in the scoreboard frame. This creates a new HTML page that uses buttons to contain the score. Notice that the initializeScoreBoard() function multiplied the runningAverage value by 100 and rounded it off-that was to make displayScoreBoard() easier to write and understand:

function displayScoreBoard()
    {
    parent.scoreboard.document.close();
    parent.scoreboard.document.open();
    parent.scoreboard.document.open();
    openScoreBoardTag("HTML");
        openScoreBoardTag("BODY");
            openScoreBoardTag("CENTER");
                openScoreBoardTag("FORM");
                    openScoreBoardTag("CODE");
                    writeScoreBoard("Average misses: ");
                    openScoreBoardTag("INPUT TYPE=\"button\" VALUE=\""
                        + Math.floor(runningAverage / 1ØØ) + "\"");
                    openScoreBoardTag("INPUT TYPE=\"button\"" +
                        " VALUE=\".\"");
                    openScoreBoardTag("INPUT TYPE=\"button\" VALUE=\""
                        + (Math.floor(runningAverage / 1Ø) % 10) +
                        "\"");
                    openScoreBoardTag("INPUT TYPE=\"button\" VALUE=\"" +
                        (Math.floor(runningAverage) % 1Ø) + "\"");
                    closeScoreBoardTag("CODE");
                closeScoreBoardTag("FORM");
            closeScoreBoardTag("CENTER");
        closeScoreBoardTag("BODY");
    closeScoreBoardTag("HTML");
    }

The displayScoreBoard() function closes the scoreboard frame and then opens it again; the open() call is made twice to get around some platform-specific browser problems. The functions openScoreBoardTag(), closeScoreBoardTag(), and writeScoreBoard() write the strings to the scoreboard frame; these functions make it easier to keep track of the logical structure of the document being written:

function writeScoreBoard(text)
    {
    parent.scoreboard.document.write(text);
    }

function openScoreBoardTag(tag)
    {
    writeScoreBoard("<" + tag + unescape("%3E"));
    }

function closeScoreBoardTag(tag)
    {
    openScoreBoardTag("/" + tag);
    }

Having set up the scoreboard, you'll need some words to display on the playingfield frame. Using the MakeArray() function from Netscape's online documentation (see Appendix F), you'll create your word list:

function MakeArray(n)
    {
    this.length = n;
    for (var i = 1; i <= n; i++)
        { 
        this[ i ] = Ø;
        }
    return this;
    }

var wordList;

function initializeWordList()
    {
    wordList = new MakeArray(1Ø);
    wordList[ Ø ] = new Word("syzygy");
    wordList[ 1 ] = new Word("pendulum");
    wordList[ 2 ] = new Word("diphtheria");
    wordList[ 3 ] = new Word("phantasm");
    wordList[ 4 ] = new Word("escutcheon");
    wordList[ 5 ] = new Word("marzipan");
    wordList[ 6 ] = new Word("quincunx");
    wordList[ 7 ] = new Word("troglodyte");
    wordList[ 8 ] = new Word("jitterbug");
    wordList[ 9 ] = new Word("vivacious");
    }

Here I've created ten words; you'd probably want to use more than that. The Word object contains a BOOLEAN flag that indicates whether the word has been played before, the word itself, and the word's length. The length of the word is accessed so frequently that it's not a bad idea to make it a property of the Word object. As with the total points and total words, let's store the played flag in a cookie; that way, the user isn't given the same word twice:

function Word(word)
    {
    var playValue = getCookie("HMW" + word);
    if (playValue == null)
        {
        this.played = false;
        createCookie("HMW"+word, "Ø");
        }
    else
        {
        if (eval(playValue) == Ø)
            {
            this.played = false;
            }
        else
            {
            this.played = true;
            }
        }
    this.word = word;
    this.wordLength = word.length;
    }

Note that the word is prefaced with "HMW"; this is to avoid accidental collisions with other cookies. If the cookie's value is "0", the word has not been played before. If the cookie isn't found, a new cookie with a value of "0" is created.

Keeping track of which words a user has guessed does place a burden on you to provide new words every so often. (Otherwise, what happens when the user has played all of the words?) But this is not necessarily a harsh task. The same kind of CGI programs that provide page hit counts can be adapted to insert a random set of words into the page.

Having defined some words, it's time to put up the gallows! The function newWord() sets up a new word. In so doing, it has to take into account that it might be called in the middle of play of another word, unless you want to lock out the user from giving up in the middle. A variable, buttonsActive, is false if there is no active game in progress and true in the middle of a game.

The newWord() function then has to select a word. Because the Math object's random() method does not work on all platforms, I used the least significant digit of the current time's seconds field to select which word to use. The word list is scanned for the nth unplayed word, where n is the digit used. If there are fewer than n unplayed words left, the set of unplayed words is reused in an "eeny meeny miny mo" fashion. If there are no unplayed words left, the user is informed of this fact.

After a word has been selected, its index is noted in a global variable, currentWord. An array of 26 BOOLEAN values is created and initialized to all false; this keeps track of which letters have been guessed. The score is set to zero, and the playing field is drawn. Finally, the buttonsActive flag is set to true-the game is afoot:

var guessedLetters;

function newWord()
    {
    if (buttonsActive == true)
       {
       if (confirm("Are you sure you want to give up?") == true)
           {
           score = 6;
           alert("The word was " + wordList[ currentWord ].word);
           gameOver();
            }
        return;
       }
    var now = new Date();
    var j = now.getSeconds() % 1Ø;
    var index = -1;
    for (var i = Ø; i <= j; i++)
        {
        for (k = Ø; k < 1Ø; k++) // loop keeps us from circling forever
            {
            index++;
            if (index >= 1Ø)
                {
                index = Ø;
                }
            if (wordList[ index ].played == Ø)
                {
                break;
                }
            if (index >= 1Ø)
                {
                index = Ø;
                }
            }
        if (wordList[ index ].played != Ø)
            {
            alert("All current words have been played!");
            return;
            }
        }
    currentWord = index;
    guessedLetters = new MakeArray(26);
    for (var k = Ø; k < 26; k++)
        {
        guessedLetters[ k ] = false;
        }
    score = Ø;
    drawPlayingField();
    buttonsActive = true;
    }

The drawPlayingField() function is responsible for redrawing the playingfield frame after each guess, as well as for drawing the initial frame. Like the displayScoreBoard() function you saw earlier, it creates a new HTML page and writes it to the playingfield frame. However, it's more dynamic than displayScoreBoard(): It uses the global variable score to determine how to draw the gallows and the hanged man, and it uses the guessedLetters array to determine which letters to fill in, and to display which letters the user has already guessed. While determining which letters to fill in, it also notes whether the user has guessed the word:

function drawPlayingField()
    {
    parent.playingfield.document.close();
    parent.playingfield.document.open();
    parent.playingfield.document.open();
    openPlayingFieldTag("HTML");
        openPlayingFieldTag("BODY");
            openPlayingFieldTag("PRE");
                writePlayingFieldLine("+---+");
                writePlayingFieldLine("|/  |");
                if (score == Ø)
                    {
                    writePlayingFieldLine("|   Ø"); // the noose
                    }
                else
                    {
                    writePlayingFieldLine("|   @"); // the head
                    }
                if (score < 2)
                    {
                    writePlayingFieldLine("|");
                    }
                else if (score == 2)
                    {
                    writePlayingFieldLine("|   |"); // body
                    }
                else if (score == 3)
                    {
                    writePlayingFieldLine("|  -|"); // body and arm
                    }
                else
                    {
                    writePlayingFieldLine("|  -|-"); // body and arms
                    }
                if (score < 5)
                    {
                    writePlayingFieldLine("|");
                    }
                else if (score == 5)
                    {
                    writePlayingFieldLine("|  /"); // left leg
                    }
                else
                    {
                    writePlayingFieldLine("|  / \\"); // both legs
                    }
                writePlayingFieldLine("|");
                writePlayingFieldLine("+======");
                openPlayingFieldTag("BR");
                var missed = Ø;
                for (var k = Ø; k < wordList[ currentWord ].wordLength;
                    k++)
                    {
                    var letter =
                        wordList[ currentWord ].word.substring(k,
                        k + 1);
                    var value = codeOf(letter);
                    if (guessedLetters[ value ] == true)
                        {
                        writePlayingField(letter);
                        }
                    else
                        {
                        writePlayingField("_");
                        missed++;
                        }
                    writePlayingField(" ");
                    }
                 openPlayingFieldTag("BR");
                 if (missed)
                     {
                     writePlayingFieldLine("Letters guessed so far:");
                     for (var x = Ø; x < 26; x++)
                         {
                         if (guessedLetters[ x ] == true)
                             {
                             writePlayingField(uc.substring(x, x + 1) +
                                 " ");
                             }
                         }
                     }
                 else
                     {
                     writePlayingFieldLine("You guessed it!");
                     gameOver();
                     }
            closePlayingFieldTag("PRE");
        closePlayingFieldTag("BODY");
    closePlayingFieldTag("HTML");
    }

The functions writePlayingField(), writePlayingFieldLine(), openPlayingFieldTag(), and closePlayingFieldTag() are very simple and similar to their counterparts used to draw the scoreboard frame:

function writePlayingFieldLine(text)
    {
    parent.playingfield.document.writeln(text);
    }

function writePlayingField(text)
    {
    parent.playingfield.document.write(text);
    }

function openPlayingFieldTag(tag)
    {
    writePlayingField("<" + tag + unescape("%3E"));
    }

function closePlayingFieldTag(tag)
    {
    openPlayingFieldTag("/" + tag);
    }

The drawPlayingField() function also uses a function, codeOf(), to translate the letters used in the word into indices to use with the guessedLetters array:

function codeOf(letter)
    {
    if (letter == "a" || letter == "A") return Ø;
    if (letter == "b" || letter == "B") return 1;
    if (letter == "c" || letter == "C") return 2;
    if (letter == "d" || letter == "D") return 3;
    if (letter == "e" || letter == "E") return 4;
    if (letter == "f" || letter == "F") return 5;
    if (letter == "g" || letter == "G") return 6;
    if (letter == "h" || letter == "H") return 7;
    if (letter == "i" || letter == "I") return 8;
    if (letter == "j" || letter == "J") return 9;
    if (letter == "k" || letter == "K") return 1Ø;
    if (letter == "l" || letter == "L") return 11;
    if (letter == "m" || letter == "M") return 12;
    if (letter == "n" || letter == "N") return 13;
    if (letter == "o" || letter == "O") return 14;
    if (letter == "p" || letter == "P") return 15;
    if (letter == "q" || letter == "Q") return 16;
    if (letter == "r" || letter == "R") return 17;
    if (letter == "s" || letter == "S") return 18;
    if (letter == "t" || letter == "T") return 19;
    if (letter == "u" || letter == "U") return 2Ø;
    if (letter == "v" || letter == "V") return 21;
    if (letter == "w" || letter == "W") return 22;
    if (letter == "x" || letter == "X") return 23;
    if (letter == "y" || letter == "Y") return 24;
    if (letter == "z" || letter == "Z") return 25;
    return 26;
    }

Now the game is almost ready to play. The user will need some input elements to pick a letter; you'll use buttons that have a name and value of each letter in the alphabet, like this:

<INPUT TYPE="button" ONCLICK="guess(this)" NAME="A" VALUE="A">

The guess() function handles the event of a user clicking on the letter. It checks whether the game is active and does nothing otherwise. The codeOf() function translates the button's value property to an index, and the guessedLetters array is checked-if the letter has been guessed, no action is taken. Otherwise, the letter, both uppercase and lowercase, is scanned for in the currently selected word. If the letter is not found, the score variable is incremented. Then the drawPlayingField() function is called to update the playingfield frame. If the score is 6, the user is informed that the game is over, and is told the word:

var lc = "abcdefghijklmnopqrstuvwxyz";
var uc = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";

function guess(button)
    {
    if (buttonsActive == false)
       {
       return;
       }
    var index = codeOf(button.value);
    if (guessedLetters[ index ] == true)
        {
        return;
        }
    guessedLetters[ index ] = true;
    var match = false;
    for (var j = Ø; j < wordList[ currentWord ].wordLength; j++)
        {
        if (wordList[ currentWord ].word.substring(j, j + 1) ==
            lc.substring(index, index + 1))
            {
            match = true;
            break;
            }
        if (wordList[ currentWord ].word.substring(j, j + 1) ==
            uc.substring(index, index + 1))
            {
            match = true;
            break;
            }
        }
    if (match == false)
        {
        score++;
        }
    drawPlayingField();
    if (score >= 6)
       {
       alert("I'm sorry, you lose; the word was " +
          wordList[ currentWord ].word);
       gameOver();
       }
    }

The gameOver() function is called when the user has guessed the word, given up, or couldn't guess the word. It turns the buttons off, adds the score to the totalPoints variable, and increments the totalWords variable. The total points and total words cookies are replaced with new values, and the word cookie is replaced with a value of "1", indicating that the word has been played. The Word object's played property is also updated. The scoreboard is then updated via the initializeScoreBoard() function:

function gameOver()
    {
    buttonsActive = false;
    totalPoints += score;
    totalWords++;
    createCookie("HMPoints", totalPoints);
    createCookie("HMCount", totalWords);
    createCookie("HMW" + wordList[ currentWord ].word, "1");
    wordList[ currentWord ].played = true;
    initializeScoreBoard();
    }

There's one last function needed: an initialization function to start the ball rolling. This is the ONLOAD event handler:

function initialize()
    {
    buttonsActive = false;
    initializeWordList();
    initializeScoreBoard();
    newWord();
    }

The complete code of the JavaScript solution is shown in Listing 9.2. A sample screen is shown in Figure 9.2.

Figure 9.2 : The JavaScript hangman game in progress.


Listing 9.2: The JavaScript hangman game
<HTML>
    <HEAD>
        <SCRIPT LANGUAGE="JavaScript">
<!--

var totalPoints;
var totalWords;
var runningAverage;

function initializeScoreBoard()
    {
    var tP = getCookie("HMPoints");
    if (tP == null)
        {
        neverPlayed();
        }
    else
        {
        var tW = getCookie("HMCount");
        if (tW == null)
            {
            neverPlayed();
            }
        else
            {
            totalPoints = eval(tP);
            totalWords = eval(tW);
            }
        }
    if (totalWords == Ø)
        {
        runningAverage = Ø;
        }
    else
        {
        runningAverage = Math.round(1ØØ * (totalPoints / totalWords));
        }
    displayScoreBoard();
    }

function getCookie(s)
    {
    var target = s + "=";
    var targetLength = target.length;
    var index = Ø;
    while (1)
        {
        var offset = index + targetLength;
        if (document.cookie.substring(index, offset) == target)
            {
            var tail = document.cookie.indexOf(";", offset);
            if (tail == -1)
                {
                tail = document.cookie.length;
                }
            return unescape(document.cookie.substring(offset, tail));
            }
        index = 1 + document.cookie.indexOf(" ", index);
        if (index == Ø || index >= document.cookie.length)
            {
            return null;
            }
        }
    return null;
    }

function neverPlayed()
    {
    totalPoints = Ø;
    totalWords = Ø;
    createCookie("HMPoints", Ø);
    createCookie("HMCount", Ø);
    }

function createCookie(name, value)
    {
    document.cookie = name + "=" + escape(value);
    }

function displayScoreBoard()
    {
    parent.scoreboard.document.close();
    parent.scoreboard.document.open();
    parent.scoreboard.document.open();
    openScoreBoardTag("HTML");
        openScoreBoardTag("BODY");
            openScoreBoardTag("CENTER");
                openScoreBoardTag("FORM");
                    openScoreBoardTag("CODE");
                    writeScoreBoard("Average misses: ");
                    openScoreBoardTag("INPUT TYPE=\"button\" VALUE=\"" +
                        Math.floor(runningAverage / 1ØØ) + "\"");
                    openScoreBoardTag("INPUT TYPE=\"button\"" +
                        " VALUE=\".\"");
                    openScoreBoardTag("INPUT TYPE=\"button\" VALUE=\"" +
                        (Math.floor(runningAverage / 1Ø) % 1Ø) + "\"");
                    openScoreBoardTag("INPUT TYPE=\"button\" VALUE=\"" +
                        (Math.floor(runningAverage) % 1Ø) + "\"");
                    closeScoreBoardTag("CODE");
                closeScoreBoardTag("FORM");
            closeScoreBoardTag("CENTER");
        closeScoreBoardTag("BODY");
    closeScoreBoardTag("HTML");
    }

function writeScoreBoard(text)
    {
    parent.scoreboard.document.write(text);
    }

function openScoreBoardTag(tag)
    {
    writeScoreBoard("<" + tag + unescape("%3E"));
    }

function closeScoreBoardTag(tag)
    {
    openScoreBoardTag("/" + tag);
    }

function MakeArray(n)
    {
    this.length = n;
    for (var i = 1; i <= n; i++)
        { 
        this[ i ] = Ø;
        }
    return this;
    }

var wordList;

function initializeWordList()
    {
    buttonsActive = false;
    wordList = new MakeArray(1Ø);
    wordList[ Ø ] = new Word("syzygy");
    wordList[ 1 ] = new Word("pendulum");
    wordList[ 2 ] = new Word("diphtheria");
    wordList[ 3 ] = new Word("phantasm");
    wordList[ 4 ] = new Word("escutcheon");
    wordList[ 5 ] = new Word("marzipan");
    wordList[ 6 ] = new Word("quincunx");
    wordList[ 7 ] = new Word("troglodyte");
    wordList[ 8 ] = new Word("jitterbug");
    wordList[ 9 ] = new Word("vivacious");
    }

function Word(word)
    {
    var playValue = getCookie("HMW" + word);
    if (playValue == null)
        {
        this.played = false;
        createCookie("HMW"+word, "Ø");
        }
    else
        {
        if (eval(playValue) == Ø)
            {
            this.played = false;
            }
        else
            {
            this.played = true;
            }
        }
    this.word = word;
    this.wordLength = word.length;
    }

var buttonsActive = false;
var currentWord;
var guessedLetters;

function newWord()
    {
    if (buttonsActive == true)
       {
       if (confirm("Are you sure you want to give up?") == true)
           {
           score = 6;
           alert("The word was " + wordList[ currentWord ].word);
           gameOver();
            }
        return;
       }
    var now = new Date();
    var j = now.getSeconds() % 1Ø;
    var index = -1;
    for (var i = Ø; i <= j; i++)
        {
        for (k = Ø; k < 1Ø; k++)
            {
            index++;
            if (index >= 1Ø)
                {
                index = Ø;
                }
            if (wordList[ index ].played == Ø)
                {
                break;
                }
            if (index >= 1Ø)
                {
                index = Ø;
                }
            }
        if (wordList[ index ].played != Ø)
            {
            alert("All current words have been played!");
            return;
            }
        }
    currentWord = index;
    guessedLetters = new MakeArray(26);
    for (var k = Ø; k < 26; k++)
        {
        guessedLetters[ k ] = false;
        }
    score = Ø;
    drawPlayingField();
    buttonsActive = true;
    }

function drawPlayingField()
    {
    parent.playingfield.document.close();
    parent.playingfield.document.open();
    parent.playingfield.document.open();
    openPlayingFieldTag("HTML");
        openPlayingFieldTag("BODY");
            openPlayingFieldTag("PRE");
                writePlayingFieldLine("+---+");
                writePlayingFieldLine("|/  |");
                if (score == Ø)
                    {
                    writePlayingFieldLine("|   Ø");
                    }
                else
                    {
                    writePlayingFieldLine("|   @");
                    }
                if (score < 2)
                    {
                    writePlayingFieldLine("|");
                    }
                else if (score == 2)
                    {
                    writePlayingFieldLine("|   |");
                    }
                else if (score == 3)
                    {
                    writePlayingFieldLine("|  -|");
                    }
                else
                    {
                    writePlayingFieldLine("|  -|-");
                    }
                if (score < 5)
                    {
                    writePlayingFieldLine("|");
                    }
                else if (score == 5)
                    {
                    writePlayingFieldLine("|  /");
                    }
                else
                    {
                    writePlayingFieldLine("|  / \\");
                    }
                writePlayingFieldLine("|");
                writePlayingFieldLine("+======");
                openPlayingFieldTag("BR");
                var missed = Ø;
                for (var k = Ø; k < wordList[ currentWord ].wordLength;
                    k++)
                    {
                    var letter =
                        wordList[ currentWord ].word.substring(k,
                        k + 1);
                    var value = codeOf(letter);
                    if (guessedLetters[ value ] == true)
                        {
                        writePlayingField(letter);
                        }
                    else
                        {
                        writePlayingField("_");
                        missed++;
                        }
                    writePlayingField(" ");
                    }
                 openPlayingFieldTag("BR");
                 if (missed)
                     {
                     writePlayingFieldLine("Letters guessed so far:");
                     for (var x = Ø; x < 26; x++)
                         {
                         if (guessedLetters[ x ] == true)
                             {
                             writePlayingField(uc.substring(x, x + 1) +
                                 " ");
                             }
                         }
                     }
                 else
                     {
                     writePlayingFieldLine("You guessed it!");
                     gameOver();
                     }
            closePlayingFieldTag("PRE");
        closePlayingFieldTag("BODY");
    closePlayingFieldTag("HTML");
    }

function writePlayingFieldLine(text)
    {
    parent.playingfield.document.writeln(text);
    }

function writePlayingField(text)
    {
    parent.playingfield.document.write(text);
    }

function openPlayingFieldTag(tag)
    {
    writePlayingField("<" + tag + unescape("%3E"));
    }

function closePlayingFieldTag(tag)
    {
    openPlayingFieldTag("/" + tag);
    }

function codeOf(letter)
    {
    if (letter == "a" || letter == "A") return Ø;
    if (letter == "b" || letter == "B") return 1;
    if (letter == "c" || letter == "C") return 2;
    if (letter == "d" || letter == "D") return 3;
    if (letter == "e" || letter == "E") return 4;
    if (letter == "f" || letter == "F") return 5;
    if (letter == "g" || letter == "G") return 6;
    if (letter == "h" || letter == "H") return 7;
    if (letter == "i" || letter == "I") return 8;
    if (letter == "j" || letter == "J") return 9;
    if (letter == "k" || letter == "K") return 1Ø;
    if (letter == "l" || letter == "L") return 11;
    if (letter == "m" || letter == "M") return 12;
    if (letter == "n" || letter == "N") return 13;
    if (letter == "o" || letter == "O") return 14;
    if (letter == "p" || letter == "P") return 15;
    if (letter == "q" || letter == "Q") return 16;
    if (letter == "r" || letter == "R") return 17;
    if (letter == "s" || letter == "S") return 18;
    if (letter == "t" || letter == "T") return 19;
    if (letter == "u" || letter == "U") return 2Ø;
    if (letter == "v" || letter == "V") return 21;
    if (letter == "w" || letter == "W") return 22;
    if (letter == "x" || letter == "X") return 23;
    if (letter == "y" || letter == "Y") return 24;
    if (letter == "z" || letter == "Z") return 25;
    return 26;
    }

var lc = "abcdefghijklmnopqrstuvwxyz";
var uc = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
var score;

function guess(button)
    {
    if (buttonsActive == false)
   { 
    return;
    }
    var index = codeOf(button.value);
    if (guessedLetters[ index ] == true)
        {
        return;
        }
    guessedLetters[ index ] = true;
    var match = false;
    for (var j = Ø; j < wordList[ currentWord ].wordLength; j++)
        {
        if (wordList[ currentWord ].word.substring(j, j + 1) ==
            lc.substring(index, index + 1))
            {
            match = true;
            break;
            }
        if (wordList[ currentWord ].word.substring(j, j + 1) ==
            uc.substring(index, index + 1))
            {
            match = true;
            break;
            }
        }
    if (match == false)
        {
        score++;
        }
    drawPlayingField();
    if (score >= 6)
{
alert("I'm sorry, you lose; the word was " +
          wordList[ currentWord ].word);
gameOver();
}
    }
    
function gameOver()
    {
    buttonsActive = false;
    totalPoints += score;
    totalWords++;
    createCookie("HMPoints", totalPoints);
    createCookie("HMCount", totalWords);
    createCookie("HMW" + wordList[ currentWord ].word, "1");
    wordList[ currentWord ].played = true;
    initializeScoreBoard();
    }

function initialize()
    {
    initializeWordList();
    initializeScoreBoard();
    newWord();
    }

//-->
        </SCRIPT>
    </HEAD>
    <BODY ONLOAD="initialize()">
        <CENTER>
            <FORM NAME="controller">
                <TABLE>
                    <TR>
                        <TD>
                            <INPUT TYPE="button" NAME="start"
                                VALUE="New Word" ONCLICK="newWord()">
                        </TD>
                        <TD>
                            <CODE>
                                <INPUT TYPE="button"
                                    ONCLICK="guess(this)" NAME="A"
                                    VALUE="A">
                                <INPUT TYPE="button"
                                    ONCLICK="guess(this)" NAME="B"
                                    VALUE="B">
                                <INPUT TYPE="button"
                                    ONCLICK="guess(this)" NAME="C"
                                    VALUE="C">
                                <INPUT TYPE="button"
                                    ONCLICK="guess(this)" NAME="D"
                                    VALUE="D">
                                <INPUT TYPE="button"
                                    ONCLICK="guess(this)" NAME="E"
                                    VALUE="E">
                                <INPUT TYPE="button"
                                    ONCLICK="guess(this)" NAME="F"
                                    VALUE="F">
                                <INPUT TYPE="button"
                                    ONCLICK="guess(this)" NAME="G"
                                    VALUE="G">
                                <INPUT TYPE="button"
                                    ONCLICK="guess(this)" NAME="H"
                                    VALUE="H">
                                <INPUT TYPE="button"
                                    ONCLICK="guess(this)" NAME="I"
                                    VALUE="I">
                                <INPUT TYPE="button"
                                    ONCLICK="guess(this)" NAME="J"
                                    VALUE="J">
                                <INPUT TYPE="button"
                                    ONCLICK="guess(this)" NAME="K"
                                    VALUE="K">
                                <INPUT TYPE="button"
                                    ONCLICK="guess(this)" NAME="L"
                                    VALUE="L">
                                <INPUT TYPE="button"
                                    ONCLICK="guess(this)" NAME="M"
                                    VALUE="M">
                                <BR>
                                <INPUT TYPE="button"
                                    ONCLICK="guess(this)" NAME="N"
                                    VALUE="N">
                                <INPUT TYPE="button"
                                    ONCLICK="guess(this)" NAME="O"
                                    VALUE="O">
                                <INPUT TYPE="button"
                                    ONCLICK="guess(this)" NAME="P"
                                    VALUE="P">
                                <INPUT TYPE="button"
                                    ONCLICK="guess(this)" NAME="Q"
                                    VALUE="Q">
                                <INPUT TYPE="button"
                                    ONCLICK="guess(this)" NAME="R"
                                    VALUE="R">
                                <INPUT TYPE="button"
                                    ONCLICK="guess(this)" NAME="S"
                                    VALUE="S">
                                <INPUT TYPE="button"
                                    ONCLICK="guess(this)" NAME="T"
                                    VALUE="T">
                                <INPUT TYPE="button"
                                    ONCLICK="guess(this)" NAME="U"
                                    VALUE="U">
                                <INPUT TYPE="button"
                                    ONCLICK="guess(this)" NAME="V"
                                    VALUE="V">
                                <INPUT TYPE="button"
                                    ONCLICK="guess(this)" NAME="W"
                                    VALUE="W">
                                <INPUT TYPE="button"
                                    ONCLICK="guess(this)" NAME="X"
                                    VALUE="X">
                                <INPUT TYPE="button"
                                    ONCLICK="guess(this)" NAME="Y"
                                    VALUE="Y">
                                <INPUT TYPE="button"
                                    ONCLICK="guess(this)" NAME="Z"
                                    VALUE="Z">
                            </CODE>
                        </TD>
                    </TR>
                </TABLE>
            </FORM>
        </CENTER>
    </BODY>
</HTML>

Improving the solution

Besides including more words and using a server-include CGI process to change the words on the fly, you can improve the game itself by adding a timer to increase pressure on the player. You could do this by adding a TEXT INPUT element to the form and displaying an incrementing timer in it. To start the timer you would need a function that should be executed from newWord(), a function to keep the timer running, and a function to stop the timer from gameOver(). You would need to modify the scoring to take the time into account-maybe double the points after some arbitrary number of seconds.

Modifying the example for your own use

Besides improving this example, you can devise other interactive games using this medium. Basically, any game that can be played one-on-one against the computer should work well in JavaScript-checkers, chess, Othello, Go, Parcheesi. With GIF or JPEG images, you could even play card games or board games such as Monopoly. The only limitation is your ability to translate the rules of the game into JavaScript code!