This exercise was created as a tech writing sample, but hey, it also made a playable Tic-Tac-Toe game, so I figured I’d put it up here for you to enjoy!
Tic-Tac-Toe has been around since at least the days of the pharaohs and has been played with sticks, pebbles, pencils and paper, but with these instructions you and your friends can play Tic-Tac-Toe on any web browser. A sample version of the game is here: My Tic-Tac-Toe Game.
The Basic Game
Tic-Tac-Toe is played on a 3-square by 3-square grid, as shown below. Players take turns placing their mark (an X or an O) until one player wins by putting three marks in a row, or until all squares are full. If all the squares are filled without getting three in a row, the game is a draw.
(click to enlarge)
How the Script Works
The pre-game stage draws a table with three columns and three rows and a label indicating whose turn it is. Each data cell within the table can have one of three values: “X,” “O,” or “blank.” At the beginning of a new game, which player goes first is chosen randomly and all cells are blank.
When a player clicks on a blank cell, the page assigns the selected cell to the player by filling in their mark, and redraws the grid.
If there is no winner and there are still blank cells, play continues, returning to step 2. If there is a winner, or there are no more blank cells, the game ends with either a message indicating the winner or that the game is a draw, and generates a “Start New Game” button.
Step One: Create a Framework
Before we can play a game, we need to have a page for it to reside on. Since this game is written in JavaScript, that means a simple HTML page. This particular page uses basic inline styles for simplicity, but your own page can use CSS to be as fancy as you like. A basic HTML shell looks like this:
Note the ID attributes of the <p> and <td> tags. Those are important, as we’ll be using those to tell the JavaScript where to place game elements. Each one must be unique, to avoid confusion.
Step Two: Draw and Populate the Grid
The HTML table you created above simply shows an empty grid. Next we will write the JavaScript necessary to put something in it! This will be done with three functions: freshGame(), updateDisplay(), and updateGrid().
freshGame();
This function initializes the variables we will be using. You may ask, “Why bother to put this in a function instead of just writing a script at the top of the page?” Good question! The answer is because we will want to have a “restart” button later, so we need to be able to re-initialize the variables at will. Fortunately, this is simply a bunch of variable declarations all at once, so the code is quite simple.
(click to enlarge)
What do all these variables do?
- player keeps track of which player’s turn it is. It will either be “X,” “O,” or “blank.”
- winState keeps track of the “state” of the game, which will change as time goes on. We are giving winState a numerical value here: 0 is a new game, 1 is a game that has started but not finished, 2 is a game that somebody has won, and 3 is a game that has ended in a draw. We could have used text values for these instead– which would be a good practice in a more complex game with a lot of potentially different gameplay states.
- grid is an array that keeps track of the contents of each cell. At the beginning of a new game, all cells are “blank,” meaning they have not been claimed by either player. There are nine cells, therefore nine slots in the array– but JavaScript arrays start at 0. To make it easier to mentally match cells with their values, the cells have IDs of “cell0,” “cell1,” “cell2,” etc.
TIP: At this point, the script has not chosen which player goes first, creating a “blank slate.” It might be tempting to choose who goes first here since you know that’s going to be determined randomly. But what if you decide later that players get to choose who goes first? Generally speaking, it’s good practice for every function to “only do ONE thing,” and in this case the one thing is “initialize the page.”
updateDisplay()
“Display” here refers to the message at the top of the game that says “You go first, Player ___” or something similar. What is shown there depends on the value of winState, and so what the function does will be chosen with a switch statement.
(click to enlarge)
- Case 0 is a completely fresh game, which means that no player has been chosen yet! But it’s not this function’s job to choose a player– that job gets kicked to a new function we will have to write later, called choosePlayer(). Right now we don’t care what choosePlayer() does, it just needs to tell us who the player is. Once that’s determined we display a message telling the player that it’s their turn. And now that the game has actually started, we will update winState to 1, or a continuing game. That’s how it will stay until someone wins or we run out of empty cells.
- Case 1 is a “normal” turn. Nobody has won but the game is not over, so we just tell the current player that it’s their turn.
- Case 2 is that somebody has won! Besides a little victory message, we want to do a couple of things. First, we want to display a button to create a new game in case the players want to play again. Second, we want to redraw the grid to hide all the other buttons so the players don’t just keep pointlessly clicking buttons. As with choosing who goes first, above, those actions are beyond the scope of this block, so we’ll send them to the displayNewGame() and upDateGrid() functions, respectively, which we will write later.
- Case 3 is a draw– all of the cells are filled, but there’s no winner. We’ll update the alert message and show the new game button with displayNewGame().
- Default is a catchall, which in the case of this game there should never be a reason to see. But it’s a good idea to have something there just in case the unexpected happens. So we’ll just show an error message and call displayNewGame() to start over.
TIP: The function is called updateDisplay(), but the <p> tag we’re sending the message to has an ID of “gameState”. It works, but could it be clearer? Should the <p> tag have an ID of “display”? Or should the function be called updateGameState()? A consistent naming convention will make your code easier to read.
updateGrid()
This is the function that populates the table, which it does by looping over the array grid, looking to see what should be in the table cell with the corresponding ID. So for example, if our table and array look like this:
Index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|---|---|
Cell ID | cell0 | cell1 | cell2 | cell3 | cell4 | cell5 | cell6 | cell7 | cell8 |
grid | “blank” | “X” | “blank” | “blank” | “X” | “O” | “X” | “O” | “blank” |
…then our grid should look like this:
(click to enlarge)
So, updateGrid() is going to have two major parts: a for loop that starts at zero and counts a number of iterations equal to the number of slots in the grid array, and a switch statement inside the loop which looks at the value of the current array slot and fills the table cell accordingly.
At this stage, we don’t yet know how the values in grid get changed, but we don’t really need to know that yet either. All we need to know is what the possible values could be. freshGame() sets them to “blank,” so we know that’s a possibility. If the value is blank, we want the players to be able to click a button to claim that cell.
We also know that the players are either “X” or “O,” so those are possible values for grid as well. Furthermore, we know that when someone wins the game, we’ll want to display a “You win!” message instead of a clickable button.
TIP: We don’t need a value for the draw condition. Why not?
- Cases X and O are simple enough: either player X or player O has claimed that cell and so it shows their label.
- Case “victory” shows up when somebody wins the game– but we don’t have any code in place for that yet. When we write the code to update the values of grid we’ll have to remember that “victory” is what goes into the grid array here!
- Default is what we want to have happen when nothing else does. In this case, that means drawing the buttons players press to claim a cell. The button will call a function, which we’ll call advanceTurn(), and that function will need to know which cell is being claimed, so we’ll send it a parameter with the current value of our loop index. (Note the variable closure ” + i + ” in order to tell JavaScript “the current value of i” rather than “the global value of i”.)
Step Three: Actually Play the Game
So far, we’ve built a grid, populated it with default values, and told the game how to behave when the grid buttons are clicked. Except… the script doesn’t actually know how to do most of that stuff, because it’s been shoved off on support functions. Oops. Let’s start writing those, shall we?
advanceTurn()
This function is our game’s big workhorse. When the player clicks to select a cell, advanceTurn() assigns that cell to the player and checks to see if the game continues or there is a winner. If the game continues, advanceTurn() switches players, but either way it then redraws everything and patiently waits for the next click.
(click to enlarge)
For all that work, advanceTurn() is actually quite small. It takes a parameter of which cell is being talked about, finds player in the global scope, and does its magic. However…
TIP: Remember that functions should only do one thing? If we assume “run the turn” is one thing, advanceTurn() as written is actually doing three. It works, but it’s not the best way to do it! How about something like this…?
(click to enlarge)
That’s an advanceTurn() function that only does one thing: advance the turn. It means creating two more new functions than we’ve already got, but when you’re building enormous scripts to handle very complex games, it’s a lot easier to fix a single broken function than it is to track down a bug buried in the middle of three other things. For something simple like this Tic-Tac-Toe game, it may seem trivial, but you might just be amazed at how complex your code can become very quickly– and how easy it is to forget what those 20 lines you wrote yesterday were supposed to do. Writing the assignCell() and changePlayer() functions we’ll leave as an exercise for the reader.
checkWinstate() and checkWinner()
All this grid-populating is great, but none of it means anything unless somebody wins the game! Or at least, the game ends in a draw. A player wins when they have selected all of the cells in a row, in a column, or diagonally across the grid. But the script has no idea what rows, columns, or diagonals are… so we have to tell it.
(click to enlarge)
We start from the assumption that the game will keep going unless there’s a reason not to. (That’s determined by our old pal winState, which spends most of the game with a value of 1.)
Since JavaScript can’t really handle complex comparisons, we’re going to send most of that thinking to checkWinner(), which takes any three spots on the grid and sees if they match. Then all we have to do is see if anything sent to checkWinner() comes back true. If it does, somebody won!
If checkWinner() comes back false, there are no winners. That means the game keeps going, right? Well, no, not necessarily. There might not be any more blank cells, which is what the else block of checkWinState() is looking for.
After everything in this big chunk of code finishes, it’s entirely possible that every test came back false– in which case, winState is still 1 and the game just keeps on going.
TIP: There is a lot of repeated code in that first if statement. Could that be cleaned up? Possibly with an array of winning combinations? What would such a thing look like?
Everything Else (The Support Functions)
This is your game’s toolbox of miscellaneous functions that don’t perform the critical functions of playing the game, but keep other parts of the code nice and neat.
(click to enlarge)
TIP: Remember the “victory” case of updateGrid(), and that we didn’t know where it came from? Here it is, quietly lurking in clearCells(), which is in turn a support function of checkWinState(). clearCells() goes through and changes every “blank” entry of grid to “victory” so that when advanceTurn() calls updateGrid() the buttons will automagically go away. Following these strings of logic is a skill you will want to develop to become a really good coder.
You’re Done! What Next…?
So we’ve got a working, playable Tic-Tac-Toe game! It probably won’t win an Origins Award, but it’s still pretty neat in its own little way. But how could you make it better? How about…
- Graphics and Sound. How about making the game look like it’s on a stone tablet, with the sound of rock-scraping-on-rock when you click a button? How about a fanfare when somebody wins– or a sad trombone when the game is a draw?
- Cats vs. Dogs. Instead of X or O, you could have anything. How about a cat’s head for X and a dog’s head for O? Or maybe a player’s name and user icon.
- Connect Four, Reversi… Battleship? There are lots of games based on selecting points on a grid, and the way the grid changes when that selection is made. As the grid gets larger, just counting from 0 to “number of squares minus one” might become clunky. Maybe implement coordinates? The possibilities are endless.