Remote Synthesis
Search my blog:
Viewing By Entry / Main
Oct 16, 2009

Advanced jQuery Selectors Part 2 - jQuery Tic-Tac-Toe

In the last tutorial I covered combining selectors, adding filters, DOM traversing and more. All of this was done in the context of an extremely simple example intended to make the examples easy to understand but close enough to the real world. In this example, I am going to take it a step further by using the same techniques to build a simple tic-tac-toe game. This example gets into some more complex code but essentially the entire game logic is a matter of combining selectors, filters and DOM traversal using jQuery. You can try the game here. Granted, your opposition (i.e. the computer) isn't smart (maybe in a Part 3) but I think it serves as a great example of working with jQuery selectors.

Building the Playing Board
The HTML for a tic-tac-toe board is extremely simple. It's just a simple table with some borders for the lines (you can see a screenshot to the right). Other than that, I have simple styled the test inside the grid so that our X's and O's appear nice and large. Finally, I added a div for a message (like "You win") and a button for restarting the game. This isn't the most complex HTML and CSS, though I am certain if you wanted to you could make it even nicer. You could also get rid of the inline styles on the table, but I decided that since each cell was slightly different, there was no real benefit to doing that.

<html>
<head>
<title>jQuery Examples</title>
<style type="text/css">
td {
   font-family:"Courier New", Courier, monospace;
   font-size: 40px;
   height:60px;
   width: 60px;
   text-align:center;
   cursor:pointer;
}
.unselected {
   /* does nothing */
}
.selectedX {
   color:#000000;
}
.selectedO {
   color:#999999;
}
#status {
   font-size: 20px;
}
</style>
<script src="scripts/jquery-1.3.2.min.js"></script>
</head>
<table id="tictactoe" cellspacing="0" cellpadding="0">
   <tr>
      <td style="border-right:1px solid #000000;border-bottom:1px solid #000000;"></td>
      <td style="border-right:1px solid #000000;border-bottom:1px solid #000000;"></td>
      <td style="border-bottom:1px solid #000000;"></td>
   </tr>
   <tr>
      <td style="border-right:1px solid #000000;border-bottom:1px solid #000000;"></td>
      <td style="border-right:1px solid #000000;border-bottom:1px solid #000000;"></td>
      <td style="border-bottom:1px solid #000000;"></td>
   </tr>
   <tr>
      <td style="border-right:1px solid #000000;"></td>
      <td style="border-right:1px solid #000000;"></td>
      <td></td>
   </tr>
</table>
<div id="status"></div>
<input type="button" name="restart" value="restart" onclick="restart()">
<body>
</body>
</html>

Setting Up the Board
Before the game begins, or when you manually restart the game, we need to set up the board. This means clearing all items and resetting all "settings" which are actually just some identifier CSS classes. The method to this is called restart() and it is triggered when the document is ready using the "document.ready()" jQuery function and also triggered by the restart button as you can see in the HTML above.

We also have a variable that indicates whether it is the user's turn or the computer's turn. I called this variable "myTurn" whereby 1 means it is my turn and 0 means it is not. We set this variable outside the method so that it is global to the application since we need it for many of our methods.

Most of the restart method is simply using a selector combination to select all TD elements within our table. We then add and remove some CSS classes, clear the contents (by setting the HTML to a non-breaking space) and adding a click() method to them that calls the method, selectCell, that places an X in your selection (more on that later). Finally, we set the status message to empty as well.

var myTurn;
$(document).ready(restart);

function restart() {
   myTurn = 1;
   var cells = $("#tictactoe td");
   $(cells).removeClass("selectedX");
   $(cells).removeClass("selectedO");
   $(cells).addClass("unselected");
   $(cells).html("&amp;nbsp;");
   $(cells).click(selectCell);
   $("#status").html("");
}

My Turn
The user's turn behavior is triggered by the click() method we added to the TD elements in our table during the restart() method. When the method is called, the "this" element within our method represents the clicked cell. The functionality for this is pretty simple, just a matter of making sure the cell is not already filled and then placing an X in the cell. We also change the CSS class from "unselected" to "selectedX," some of why this is done becomes obvious in the logic for the computers turn and how we determine a winner.

If no one has won yet, we set the myTurn method to indicate false and call the computers turn logic. We'll delve into the logic for determining the winner later - this is where the guts of the application's logic reside and the heavy usage of jQuery selectors, traversing and more.

function selectCell() {
   if (myTurn == 1 && $(this).html() != "X" && $(this).html() != "O") {
      $(this).html("X");
      $(this).removeClass("unselected");
      $(this).addClass("selectedX");
      if (!someoneHasWon($(this))) {
         myTurn = 0;
         computersTurn();
      }
   }
}

Computer's Turn
It's important to note that while the computer's turn logic is more complex than the user's turn, the computer is a "dumb" opponent in our current implementation. Basically, the computer will randomly choose an empty cell and place an O in there. It does not weigh whether there is an opportunity to win or block the opponent, making it pretty easy for the user to win every time. As you'll see, it's chances of making a "good" move are purely random.

The only real selector logic going on within the computersTurn() method is getting all the unselected cells. As you may recall, in the restart() method, we default the CSS class on all the TD elements in our grid to "unselected." When a user clicks on a cell, we remove "unselected" and place "selectedX." Likewise, when the computer places an O, we place a CSS class of "selectedO."

Both of these classes do handle some styling differences, but also indicate that a cell is unavailable for a move. This allows us to easy use the selector $("#tictactoe td.unselected") to quickly get all of the available cells in which to move. Once we do that, we simply use the JavaScript Math class to get a random number within the range of the available number of cells. The computer selects that random cell, we set and remove the appropriate classes and, if no one has won yet, indicate that it is again the user's turn.

function computersTurn() {
   unselectedCells = $("#tictactoe td.unselected");
   var item;
   if (unselectedCells.length > 1) {
      randomItem = Math.floor(Math.random() * unselectedCells.length); // choose a random item
      item = unselectedCells[randomItem];
   }
   else if (unselectedCells) {
      item = $(unselectedCells);
   }
   $(item).html("O");
   $(item).removeClass("unselected");
   $(item).addClass("selectedO");
   if (!someoneHasWon(item)) {
      myTurn = 1;
   }
}

Determining the Winner
Alright; here's where we get into the fun part of this little example application, particularly when it comes to utilizing a variety of selectors, filters and traversing techniques within jQuery. You see, pretty much all of the logic for determining a winner in tic-tac-toe can be done using selectors. Some of the logic, as you'll see, is pretty simple - for example, determining if there is a winner on a horizontal row. Others, like determining a winner along the diagonals, we a little trickier but the power of jQuery selectors made all this possible with minimal code.

Our function, someoneHasWon(), expects you to pass the currently selected item (i.e. TD element). The reason for this is that a winning move would always be relative to the currently selected item and we don't need to waste time looking at rows and columns where our item is not present.

We begin by assuming that no one has won and thereby setting our variable "winner" to false. Next, we determine whether the currently selected item is an X or an O (i.e. XorO) by getting the HTML contents of the passed item; we use this to compare to the other items to see if they match.

The next line traverses the DOM to determine which spot within the current table row (TR) element our current item is. Obviously, the parent of our current TD element is the TR and we use the index() method to tell us which of the three spots we are in. This is needed to later evaluate whether this move resulted in a win vertically.

The final variables set have to do with determining if the move resulted in a win across one of the diagonals. The variable "diagItems" contains an array of all of the diagonal items which are easily retrieved by using the ":even" filter on our TD selectors. As you may notice from the graphic above, in an array starting at 0, all of the diagonals fall into an even numbered index, 0,2,4,6 and 8. Once we have an array of diagonal items, we can use the index() method again to determine if the current cell is within a diagonal and at what position (i.e. inDiagItemsIndex).

We'll begin by determining if there is a winner across the current row since that is the easiest by far. In order to do this, we simply get the siblings of our currently selected TD element (i.e. the other cells in the row) using the siblings() method and see if they contain the same character (i.e. X or O) as our selected cell. If we get back an array with two elements, meaning both siblings have the same character, then we know we have a complete row.

Next we'll evaluate whether there is a winner in our current column. As you'll recall, earlier we set a variable for the index of the current cell in its row (thisIndex). We use some DOM traversal to select the parent of our current cell (i.e. the TR element), then get the row above and below and check the child cell in the same index as ours to see if it contains the same element (X or O) as our selected cell. For example, the selector $(currentItem).parent().siblings(":first").children(":eq("+thisIndex+")").html() gets the contents of the cell directly above or below our current cell depending on which row you are in.

Now we are ready to examine the diagonals, assuming our selected cell is inside one (indicated by inDiagItemsIndex). Our variable "diagItems" contains the array of diagonal items highlighted in the graphic above. You will notice that the right-to-left diagonal would be the second, third and fourth items items included in an array of diagonal items. Thus, to determine if we have a winning move in the right-to-left diagonal we simply utilize the slice() method to get those array elements and then filter them by those containing the same element (X or O) as our current cell (i.e. $(diagItems).slice(1,4).filter(":contains('"+XorO+"')")).

To do the same for the left-to-right diagonal, we simply filter our array of diagonals by only even numbered indexes since the left-to-right diagonal would be elements 0, 2 and 4 (i.e. $(diagItems).filter(":even").filter(":contains('"+XorO+"')")).

Finally, if any of these checks have determined a winner, we set the contents of the status message to display who the winner is. In order to prevent any further moves by the user, we remove the click() function from our TD elements by saying $("#tictactoe td").unbind().

function someoneHasWon(currentItem) {
   var winner = false;
   var XorO = $(currentItem).html();
   var thisIndex = $(currentItem).parent().children().index($(currentItem));
   var diagItems = $("#tictactoe td:even");
   var inDiagItemsIndex = $(diagItems).index($(currentItem)); // if its in a diagonal, we check for diagonal

wins
   // items in this row are the same as the current item
   if ($(currentItem).siblings(":contains("+XorO+")").length == 2) {
      winner = true;
   }
   // items above and/or below
   else if ($(currentItem).parent().siblings(":first").children(":eq("+thisIndex+")").html() == XorO &&

$(currentItem).parent().siblings(":last").children(":eq("+thisIndex+")").html() == XorO) {
      winner = true;
   }
   else if (inDiagItemsIndex >= 0) {
      // if its in the / diagonal
      if ($(diagItems).slice(1,4).filter(":contains('"+XorO+"')").length == 3) {
         winner = true;
      }
      // if its the \ diagonal
      if ($(diagItems).filter(":even").filter(":contains('"+XorO+"')").length == 3) {
         winner = true;
      }
   }
   if (winner == true) {
      $("#status").html("The winner is " + $(currentItem).html() + ".");
      $("#tictactoe td").unbind(); // click does nothing now
   }
   return winner;
}

Looking Ahead
Hopefully this fun little example proves a decent example of the complexity you are able to achieve though the use of selectors, filters and DOM traversal. As you hopefully noticed from the final section above, the core logic of a tic-tac-toe game was almost completely a combination of jQuery selectors, filters and DOM traversal. Using some creative techniques we were able to get every cell of our tic-tac-toe board without needing to put unnecessary ID's on every cell, which allows us to keep our HTML clean.

As I said at the beginning of the post, I think there is a relatively straightforward way I can use jQuery (specifically I have the data() method in mind) to make your computer opponent more intelligent. It would be a fun experiment, if entirely useless, and if I do get around to it, I will be sure to follow up this post with a part 3.

Comments
Josh
You should update the link for the example, currently the file isn't there


Brian Rinaldi
Apologies. The link has been corrected. Thanks for bringing that to my attention.


jakfrost
curious as to why you made several calls to $(cells) in the 'restart' function. is there any performance difference between doing:
  $("#tictactoe td")
    .removeClass("selectedX")
    .removeClass("selectedO")
    .addClass("unselected")
    .html("&nbsp;");

and making the 'restart' function like you did?


Brian Rinaldi
You know I don't know about any performance implications though I suspect there isn't one. It's just a habit - you could do it either way.


Write your comment



(it will not be displayed)