Previous Page TOC Next Page See Page



- 18 -
JavaScript Solitaire


Given JavaScript's utility for adding interactivity to Web pages and HTML forms, it is ideally suited for implementing games.

In fact, some of the earliest examples of JavaScript on the Web were games ranging from a simple matches game to a full-text fantasy adventure.

In this chapter, you will see how to implement a simple solitaire card game, more as an example of interactive forms than an example of complex game development. This game demonstrates several key JavaScript principles, including the following:

The chapter begins by examining the game you will be implementing and the requirements this game has in terms of an interface. Then the actual program code and the way it works is reviewed.

The Game


The solitaire game being implemented in this chapter is fairly straightforward:

  1. The deck is shuffled.

  2. Four cards are dealt face-up in a row.

  3. If two cards on the top of any of the four piles form a pair (in other words, the same number—suit is irrelevant), they are discarded.

  4. When all pairs are discarded, step two is repeated, dealing on top of the previous cards—if the deck is finished, the game is over.

  5. When the deck is finished, if all cards have been discarded, the game is won.

The game is admittedly simple. It requires little in the way of strategy on the part of the user and is easy to learn. For the purposes of this chapter, however, it provides a good example of how to implement an interactive game using JavaScript. And it isn't the worst way to pass idle hours.

Basic Requirements


The key to implementing the game lies in its interface. The game must provide the following:

In addition, the user should be informed that the deck is being shuffled and should be told that no cards are left to deal if the user attempts to deal more cards when all cards have been dealt. Any attempt to remove an invalid pair should simply be ignored by the program.

Source Code for the Game


The entire game is implemented in one HTML file with embedded JavaScript:

Listing 18.1. A Solitaire Game.

<HEAD>
<TITLE>
JavaScript Solitaire
</TITLE>
<SCRIPT LANGUAGE="JavaScript">
var cards = "-A23456789TJQK";
var suits = "-HDCS";
var deck = new Array(52);
var card1 = new Array(14);
var card2 = new Array(14);
var card3 = new Array(14);
var card4 = new Array(14);
var cardPoint1, cardPoint2, cardPoint3, cardPoint4;
var currentCard = 0;
var choice1, choice2, value1, value2;
var hands;
var taken = 0;
for (i = 0; i < 52; i++) {
    deck[i] = new cardObj();
}
for (i = 0; i < 14; i++) {
    card1[i] = new cardObj();
    card2[i] = new cardObj();
    card3[i] = new cardObj();
    card4[i] = new cardObj();
}
function cardObj() {
    this.card = -1;
    this.suit = -1;
}
function shuffleDeck() {
    var chooseCard, chooseSuit;
    var dialog = window.open("","dialogBox","height=100,width=300");
    dialog.document.open("text/html");
    dialog.document.writeln("<HEAD><TITLE>");
    dialog.document.writeln("Shuffling cards");
    dialog.document.writeln("</TITLE></HEAD>");
    dialog.document.writeln("<BODY BGCOLOR=#020A33 TEXT=cornsilk>");
    dialog.document.writeln("<H1><DIV ALIGN=CENTER>");
    dialog.document.writeln("Shuffling cards ... please wait");
    dialog.document.writeln("</DIV></H1></BODY>");
    dialog.document.close();
    for (i = 0; i < 52; i++) {
        deck[i].card = -1;
        deck[i].suit = -1;
    }
    for (i = 0; i < 52; i++) {
        chooseCard = Math.ceil(Math.random() * 13);
        chooseSuit = Math.ceil(Math.random() * 4);
        while (cardSelected(chooseCard,chooseSuit,i)) {
            chooseCard = Math.ceil(Math.random() * 13);
            chooseSuit = Math.ceil(Math.random() * 4);
        }
        deck[i].card = chooseCard;
        deck[i].suit = chooseSuit;
    }
    dialog.close();
}
function cardSelected(chosenCard,chosenSuit,currentCounter) {
    for (j = 0; j < currentCounter; j++) {
        if (deck[j].card == chosenCard && deck[j].suit == chosenSuit) {
            return true;
        }
    }
    return false;
}
function takeCards() {
    getChoices();
    if (checkCards()) {
        removeCards();
    }
    if (checkWin()) {
        if (userWon()) {
            startGame();
        } else {
            self.location = "solbye.html";
        }
    }
}
function userWon() {
    return confirm("You won! Play again?");
}
function checkWin() {
    if (taken == 52) { return true; }
    return false;
}
function getChoices() {
    choice1 = 0;
    while (choice1 < 4) {
        if (document.game.choice1[choice1].checked) { break; }
        choice1 ++;
    }
    choice1++
    choice2 = 0;
    while (choice2 < 4) {
        if (document.game.choice2[choice2].checked) { break; }
        choice2 ++;
    }
    choice2++
    if (choice1 == 5 || choice2 == 5) {
        choice1 = 5;
        choice2 = 5;
        value = 0;
        value = 0;
        return;
    }
    value1 = eval("card" + choice1 + "[cardPoint" + choice1 + "].card");
    value2 = eval("card" + choice2 + "[cardPoint" + choice2 + "].card");
}
function checkCards() {
    if (choice1 == choice2) { return false; }
    if (value1 != value2) { return false; }
    if (value1 == 0 || value2 == 0) { return false; }
    return true;
}
function removeCards() {
    eval("cardPoint" + choice1 + "--");
    eval("cardPoint" + choice2 + "--");
    eval("document.game.choice1[" + --choice1 + "].checked = false");
    eval("document.game.choice2[" + --choice2 + "].checked = false");
    choice1 = 0;
    choice2 = 0;
    taken += 2;
    document.game.taken.value = taken;
    displayCards();
}
function dealCards() {
    if (hands == 13) {
        alert ("All the cards have been dealt");
        return;
    }
    hands++;
    document.game.hands.value = 52 - (hands * 4);
    card1[++cardPoint1] = deck[currentCard++];
    card2[++cardPoint2] = deck[currentCard++];
    card3[++cardPoint3] = deck[currentCard++];
    card4[++cardPoint4] = deck[currentCard++];
    displayCards();
}
function displayCards() {
    if (cardPoint1 == 0) {
        document.game.card1.value = "";
    } else {
        document.game.card1.value = cards.charAt(card1[cardPoint1].card);
        document.game.card1.value += " of ";
        document.game.card1.value += suits.charAt(card1[cardPoint1].suit);
    }
    if (cardPoint2 == 0) {
        document.game.card2.value = "";
    } else {
        document.game.card2.value = cards.charAt(card2[cardPoint2].card);
        document.game.card2.value += " of ";
        document.game.card2.value += suits.charAt(card2[cardPoint2].suit);
    }
    if (cardPoint3 == 0) {
        document.game.card3.value = "";
    } else {
        document.game.card3.value = cards.charAt(card3[cardPoint3].card);
        document.game.card3.value += " of ";
        document.game.card3.value += suits.charAt(card3[cardPoint3].suit);
    }
    if (cardPoint4 == 0) {
        document.game.card4.value = "";
    } else {
        document.game.card4.value = cards.charAt(card4[cardPoint4].card);
        document.game.card4.value += " of ";
        document.game.card4.value += suits.charAt(card4[cardPoint4].suit);
    }
}
function startGame() {
    document.game.choice1[0].checked = false;
    document.game.choice1[1].checked = false;
    document.game.choice1[2].checked = false;
    document.game.choice1[3].checked = false;
    document.game.choice2[0].checked = false;
    document.game.choice2[1].checked = false;
    document.game.choice2[2].checked = false;
    document.game.choice2[3].checked = false;
    hands = 0;
    taken = 0;
    document.game.taken.value = taken;
    currentCard = 0;
    cardPoint1 = 0;
    cardPoint2 = 0;
    cardPoint3 = 0;
    cardPoint4 = 0;
    card1[cardPoint1].card = "";
    card2[cardPoint2].card = "";
    card3[cardPoint3].card = "";
    card4[cardPoint4].card = "";
    shuffleDeck();
    dealCards();
}
</SCRIPT>
</HEAD>
<BODY onLoad="startGame()" BGCOLOR="white" FGCOLOR="cyan">
<FORM NAME="game">
<DIV ALIGN=CENTER>
<TABLE BORDER=0 CELLPADDING=5>
    <TR>
        <TD COLSPAN=6 ALIGN=CENTER BGCOLOR="midnightblue">
            <FONT SIZE=6 COLOR=yellow>Solitaire</FONT>
        </TD>
    </TR>
    <TR>
        <TD ALIGN=CENTER BGCOLOR="darkmaroon">
            <FONT SIZE=4 COLOR=white>CARDS</FONT>
        </TD>
        <TD ALIGN=CENTER BGCOLOR="darkgreen">
            <INPUT TYPE=text NAME="card1" SIZE=6 onFocus="this.blur()">
        </TD>
        <TD ALIGN=CENTER BGCOLOR="darkgreen">
            <INPUT TYPE=text NAME="card2" SIZE=6 onFocus="this.blur()">
        </TD>
        <TD ALIGN=CENTER BGCOLOR="darkgreen">
            <INPUT TYPE=text NAME="card3" SIZE=6 onFocus="this.blur()">
        </TD>
        <TD ALIGN=CENTER BGCOLOR="darkgreen">
            <INPUT TYPE=text NAME="card4" SIZE=6 onFocus="this.blur()">
        </TD>
        <TD ALIGN=CENTER BGCOLOR="darkgray" ROWSPAN=7>
            <FONT SIZE=4 COLOR=darkmaroon>CARDS<BR>LEFT<BR>IN<BR>DECK:</FONT><BR>
            <INPUT TYPE=text NAME="hands" SIZE=3 onFocus="this.blur()"><P>
            <HR><P>
            <FONT SIZE=4 COLOR=darkmaroon>CARDS<BR>TAKEN:</FONT><BR>
            <INPUT TYPE=text NAME="taken" SIZE=3 onFocus="this.blur()"><P>
        </TD>
    </TR>
    <TR>
        <TD ALIGN=CENTER BGCOLOR="darkmaroon">
            <FONT SIZE=4 COLOR=white>CHOICE 1</FONT>
        </TD>
        <TD ALIGN=CENTER BGCOLOR="cornsilk">
            <INPUT TYPE=radio NAME="choice1" VALUE="1">
        </TD>
        <TD ALIGN=CENTER BGCOLOR="cornsilk">
            <INPUT TYPE=radio NAME="choice1" VALUE="2">
        </TD>
        <TD ALIGN=CENTER BGCOLOR="cornsilk">
            <INPUT TYPE=radio NAME="choice1" VALUE="3">
        </TD>
        <TD ALIGN=CENTER BGCOLOR="cornsilk">
            <INPUT TYPE=radio NAME="choice1" VALUE="4">
        </TD>
    </TR>
    <TR>
        <TD ALIGN=CENTER BGCOLOR="darkmaroon">
            <FONT SIZE=4 COLOR=white>CHOICE 2</FONT>
        </TD>
        <TD ALIGN=CENTER BGCOLOR="cornsilk">
            <INPUT TYPE=radio NAME="choice2" VALUE="1">
        </TD>
        <TD ALIGN=CENTER BGCOLOR="cornsilk">
            <INPUT TYPE=radio NAME="choice2" VALUE="2">
        </TD>
        <TD ALIGN=CENTER BGCOLOR="cornsilk">
            <INPUT TYPE=radio NAME="choice2" VALUE="3">
        </TD>
        <TD ALIGN=CENTER BGCOLOR="cornsilk">
            <INPUT TYPE=radio NAME="choice2" VALUE="4">
        </TD>
    </TR>
    <TR>
        <TD ALIGN=CENTER ROWSPAN=4 BGCOLOR="darkmaroon">
            <FONT SIZE=4 COLOR=white>CONTROLS</FONT>
        </TD>
        <TD COLSPAN=4 ALIGN=CENTER BGCOLOR="black">
            <FONT COLOR=white>
            <INPUT TYPE=button NAME="take" VALUE="TAKE THE PAIR" onClick="takeCards()">
            </FONT>
        </TD>
    </TR>
    <TR>
        <TD COLSPAN=4 ALIGN=CENTER BGCOLOR="black">
            <FONT COLOR=white>
            <INPUT TYPE=button NAME="deal" VALUE="DEAL CARDS" onClick="dealCards()">
            </FONT>
        </TD>
    </TR>
    <TR>
        <TD COLSPAN=4 ALIGN=CENTER BGCOLOR="black">
            <FONT COLOR=white>
            <INPUT TYPE=button NAME="start" VALUE="START OVER" onClick="startGame()">
            </FONT>
        </TD>
    </TR>
    <TR>
        <TD COLSPAN=4 ALIGN=CENTER BGCOLOR="black">
            <FONT COLOR=white>
            <INPUT TYPE=button NAME="exit" VALUE="EXIT" onClick="self.location = 'solbye.html'">
            </FONT>
        </TD>
    </TR>
</TABLE>
</DIV>
</FORM>
</BODY>

The Interface Form


The first piece of the script to look at is the form that provides the user interface.

The form uses tables and Navigator 3's ability to display different color backgrounds in separate table cells to produce the colorful layout seen in Figure 18.1.

Figure 18.1. The Solitaire user interface.

The form itself provides all the fields and buttons described in the basic requirements section earlier in this chapter.

The four cards are each displayed as text labels in three text input fields called card1, card2, card3, and card4. Two sets of radio buttons named choice1 and choice2 respectively enable the user to select two cards. An additional two text fields called hands and taken provide display space for the remaining cards and discarded cards counters.

The six text fields all have the same event handler to ensure that users can't change the values of these fields:

onFocus="this.blur()"

Finally, four buttons are provided:

In addition to the form, the HTML <BODY> tag has an event handler that calls the startGame() function.

Global Variables and Objects


Before looking at the various functions that make up the script, you need to look at the global variables and object definitions used throughout the application.

All the major information in the script is stored in global variables so that it is easily accessible to all functions without having to pass arguments back and forth throughout the script. This makes the data more susceptible to mistaken coding in one function corrupting data needed by the rest of the script, but it makes writing the script easier.

At the top of the script, the following global variables and constants are defined:

The script also uses one custom object mentioned above: cardObj. This object has two properties—card and suit, used to store the value and suit of a card. These properties provide an easy way to get at the two attributes of any card.

The startGame() Function


The startGame() function is called when the page first loads from the onLoad event handler of the <BODY> tag. It also is called when the user clicks on the Start Over button.

The function first initializes several variables:

After these values are initialized, shuffleDeck() is called to shuffle the cards and then dealCards() is called to deal the first four cards.

The shuffleDeck() Function


The shuffleDeck() function attempts to shuffle the deck using the Math.random() method.

The function starts by opening a window to inform the user that the deck is being shuffled like the one shown in Figure 18.2. This is done because on slower systems the shuffling process causes a noticeable delay in the program.

Figure 18.2. A window is used to inform the user that shuffling is occurring.

Before shuffling occurs, the card and suit of each card in the deck is cleared to a value of -1, indicating that no valid card has been assigned.

The shuffling process takes place within a for loop which iterates from 0 to 51—each entry in the deck array.

For each entry in the array, the following process is used to select a card: An initial card is selected using Math.random(), and then a while loop is used to continue selecting cards until a card that hasn't been selected is found.

This test to see if the randomly selected card has already been used is done by calling cardSelected() with three arguments: the value and suit of the card plus the index number of the card in the deck.

Once the card is selected, the function closes the message window.

The cardSelected() Function


This function is simple. It loops through each card in the deck up to the card currently being selected and compares the card and suit property to values of the arguments passed to the function. If any card matches, then false is returned by the function, otherwise true is returned.

The dealCards() Function


After calling the shuffleDeck() function, the startGame() function finished by calling dealCards() to display the first four cards of the deck. This function is also called by the onClick event handler of the Deal Cards button in the form to display the next four cards.

The first step taken by the function is to check the value of the hands counter. If it is 13, then all cards have been dealt and an alert dialog like the one shown in Figure 18.3 is used to inform the user, and the function exits.

Figure 18.3. An alert box is used to inform the user when there are no more cards are left to deal.

Otherwise the dealing process begins. First, the hands counter is incremented by one to reflect a new set being dealt and the value is used to calculate and display the number of cards remaining in the deck for the user.

Next, the four cards are dealt from the deck array into the card1, card2, card3, and card4 arrays.

This is done with four similar commands like this one, used to deal into the first pile:

card1[++cardPoint1] = deck[currentCard++];

What's worth noting here is the differing uses of the unary increment operator. In the left side of the expression, the counter variable is incremented before using the value because the value of the counter represents the currently displayed card. On the right side, however, the value is used before incrementing because the counter currentCard points to the next available card for dealing.

The last step in the dealCards() function is to call displayCards() to display the results of dealing the cards.

The displayCards() Function


This function is called by dealCards() after a new set of cards has been dealt and by removeCards() after a pair is successfully removed by the user.

For each pile, the following steps are taken: if the counter for the current pile (cardPoint1, and so on) is zero, then the pile is empty and an empty string is assigned to the value property of the text field. Otherwise, the card property for the card object pointed to by the counter is used as an argument to the charAt() method of the cards string, as is the suit property used for the suits string to display the card's name in the appropriate field in the form.

The takeCards() Function


The takeCards() function is called by the appropriate button in the form.

The function starts by calling getChoices() to identify the cards selected by the user. Then the cards are checked by calling checkCards(). If the cards are valid, then removeCards() is called to remove the cards.

Next, a check is made to see if the user has won by calling checkWin(). If they have, userWon() is called to inform the user and if the user wants to continue, startGame() is called. If the user doesn't want to continue, then the farewell page is loaded.

The getChoices() Function


This function extracts information about the user's selected cards.

The function first gets the pile numbers of the two selections (a value from one to four). This is accomplished by looking for the index number of the button where the checked property is true and then adding one to the value. A while loop increments the index until a selected button is found.

After the loop, the index is incremented by one. If no index had been found, the index will be four and be incremented to become five, an invalid value, but one that is handled by the next step in the function. This step is to see if either index is five. If it is, both are set to five, the values of the cards are set to zero, and the function exits.

If the two piles are valid selections, then the card values for the piles are assigned to value1 and value2 using the eval() statement. This statement takes a string and evaluates its content as a JavaScript operation. In this case, the card property of the top card is evaluated in a given pile using the value of choice1 or choice2 to select the right array name to work with.

The checkCards() Function


The checkCards() function performs three basic checks to confirm the validity of the cards selected by the user. If choice1 is the same as choice2 (in other words, the user has selected the same card twice), or value1 is different than value2 (meaning that the user has selected cards with different values), or either value1 or value2 is equal to zero (meaning that the user has selected at least one empty pile or forgotten to select one of the cards) then the cards are invalid and false is returned.

Otherwise, true is returned.

The removeCards() Function


This function is used to discard a pair of cards if the user has made a valid selection.

First, the counters for the two piles are decreased by one by using an eval() statement to evaluate the names of the pile arrays based on the values stored in choice1 and choice2.

Then, eval() statements are used to uncheck the two radio buttons.

Next, choice1 and choice2 are cleared to zero and the taken counter is incremented by two and displayed. Finally, displayCards() is called.

The checkWin() Function


This function checks to see if the user has won by seeing if all 52 cards have been taken (this is accomplished by looking at the value of the taken counter).

The userWon() Function


This function is called if the user has won. It informs the user of the win and asks if the user if he or she wants to play again. It does this by returning the value of a single confirm() method call.

Summary


In this chapter you were shown how it is possible to implement an interactive game using forms and basic JavaScript. This game provided examples of event handlers, the eval() statement, dynamic form updating, and the random() method.

In the next chapter you will be shown the development of a unified interface to three leading search engines. JavaScript is used to implement a single form that searches all three engines without requiring any server-side scripting.

Previous Page Page Top TOC Next Page See Page