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("&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.
$("#tictactoe td")
.removeClass("selectedX")
.removeClass("selectedO")
.addClass("unselected")
.html(" ");
and making the 'restart' function like you did?
