Step-by-Step Guide to Coding Tic Tac Toe in JavaScript

Step-by-Step Guide to Coding Tic Tac Toe in JavaScript

In this post we will be building the ever popular Tic Tac Toe game in JavaScript. It's a relatively simple implementation and the most complex portion is the calculation of a win scenario. If you are relatively new to JavaScript, then I highly recommend Web Design with HTML, CSS, JavaScript and jQuery as a good starting point.

I normally start with zero code written, and I don't normally use 3rd party libraries for these things. So there are no 3rd party libraries or frameworks to setup. All you will need is your favorite coding editor, like VS Code.

Before we get into the code, here is a running version of what we will be building.

Tic Tac Toe
Player 1
0
XX
O
Player 2
0

The rules of the game are as follows.

In Tic Tac Toe, 2 players take turns adding their token (an X or a O) to a 3 x 3 (or N X N, for the more ambitious) grid until one player matches 3 (N) in a row in any direction.

Let's set up a few basic variables to keep track of the game settings.

Variable Declaration

var winners = new Array();
var player1Selections = new Array();
var player2Selections = new Array();
var currentPlayer = 0;
var points1 = 0;    // player 1 points
var points2 = 0;    // player 2 points
var size = 3;

Here is a breakdown of each of the variables.

winners - This variable will be an array containing all of the possible combinations that are considered 'winning'.

player1Selections - We will use this array to store the blocks that player1/player2 have selected.

currentPlayer - This is the index of the player currently in play, starting with 0.

points1/points2 - These variables will be used to keep track of the ongoing games player scores.

size - This is the size of the playing board measure in width * height.

Next up let us draw the tic tac toe board, which is an n x n table. In this case that n will be a 3. The CSS for the elements can be found after the post at the bottom of the page.

<div class="game">
    <div class="game-title" contenteditable="true">Tic Tac Toe</div>
    <div class="player" contenteditable="true">
        Player 1
        <div style="font-size:30pt;" id="player1" class="selected" contenteditable="true">
        0
        </div>
    </div>
            
    <table id="game" class="tictactoe" style="float:left;width:45%;" contenteditable="true">
    </table>
            
    <div class="player" contenteditable="true">
        Player 2
        <div id="player2" style="font-size:30pt;" contenteditable="true">0</div>
    </div>
            
    <div class="clear" contenteditable="true"></div>
</div>

And the following function will be in charge of rendering the n x n grid onto the webpage.

// JavaScript
function drawBoard()
{
    let parent = document.getElementById("game");
    let counter = 1;

    for (let i = 0; i < 3; i++)
    {
        let row = document.createElement("tr");

        for(let x = 0; x < size; x++)
        {
            let col = document.createElement("td");
            col.innerHTML = counter;

            row.appendChild(col);
        }
        parent.appendChild(row);
    }
}

Every major step in the game is its own function. It makes it easier to edit, as changing certain parts doesn't interfere with the rest of the game elements. It also makes it much easier to test in the debugger when you can just call a function and see the outcome of that snippet immediately.

So now we have a game board. And there's not much else to do with that really. We can add a few basic game elements, like the user scores and that will look something like this.


Player 1
0
1 2 3
4 5 6
7 8 9
Player 2
0

After a win condition is met, the winning player will get a point added to their score and the game board will reset itself.

Let's break down what happens when a user makes a selection first, however. And we also need to figure out what a winning case actually is, so let's start there.

What is a winning condition?

For an n x n tic tac toe game, a winning condition is matching n selections in a straight line either horizontal, vertical, or diagonal. We don't have to start checking for a win until any user has selected 3 or more cells however, because you obviously can't win with just 2 or less elements placed on the board.

And here's the easy part. For a 3 x 3 grid, we can see what those selections will look like the following. 1,2,3 or 4,5,6, or 7,8,9.

So we have 3 sets of cells to compare against.

Horizontal: [1, 2, 3] [4, 5, 6] [7, 8, 9]

Vertical: [1, 4, 7] [2, 5, 8] [3, 6, 9]

Diagonal: [1, 5, 9] [3, 5, 7]

Any user having any of these at any point is the winner and gets a point. We'll add another variable to the list to keep these winning selections stored.

var winners = new Array();

function loadAnswers()
{
    winners.push([1, 2, 3]);
    winners.push([4, 5, 6]);
    winners.push([7, 8, 9]);
    winners.push([1, 4, 7]);
    winners.push([2, 5, 8]);
    winners.push([3, 6, 9]);
    winners.push([1, 5, 9]);
    winners.push([3, 5, 7]);
}

Ideally, we would want these permutations to be calculated automatically, as they only currently apply to a 3 x 3 grid. If we wanted create a 4 x 4 grid for example, the amount of permutations would be much too high to calculate by hand.

We'll leave that as a challenge for you to take on.

So now we have a way to check if a player has won the game. But we'll need to keep track of which selections each players has. We don't want to have to go through the entire grid each time a player makes a selection to check if it's an 'X' or a 'O'. So we'll add 2 new variables to my list.

var player1Selections = new Array();
var player2Selections = new Array();

These arrays will keep track of which 'boxes' each user has selected.

Up next we will add an event handler to each cell click, alternating players after each one, until a winner is found, or until we run out of cells to click on. Here's a new updated version of the drawBoard() function from above.

function drawBoard() {
    var Parent = document.getElementById("game");
    var counter = 1;
    
    while (Parent.hasChildNodes()) {
        Parent.removeChild(Parent.firstChild);
    }

    for (s = 0; s < 3; s++) {
        var row = document.createElement("tr");
        
        for (r = 0; r < 3; r++) {
            var col = document.createElement("td");
            col.id = counter;
            col.innerHTML = counter;

            var handler = function(e) {
                if (currentPlayer == 0) {
                    this.innerHTML = "X";
                    player1Selections.push(parseInt(this.id));
                    player1Selections.sort(function(a, b) { return a - b });
                }

                else {
                    this.innerHTML = "O";
                    player2Selections.push(parseInt(this.id));
                    player2Selections.sort(function(a, b) { return a - b });
                }

                if (checkWinner())
                {
                    if(currentPlayer == 0)
                        points1++;
                    else
                        points2++;

                    document.getElementById("player1").innerHTML = points1;
                    document.getElementById("player2").innerHTML = points2;

                    reset();
                    drawBoard();
                }

                else
                {
                    if (currentPlayer == 0)
                        currentPlayer = 1;
                    else
                        currentPlayer = 0;
                    this.removeEventListener('click', arguments.callee);
                }
            };

            col.addEventListener('click', handler);

            row.appendChild(col);
            counter++;
        }

        Parent.appendChild(row);
    }

    loadAnswers();
}

The main additions to the function are the ability to store the players selections, adding the event handlers and checking for a winning condition. If a winning condition is found, the points get updated and then we reset the game variables and redraw the board as mentioned above.

This is why it's useful to break down the steps into as many functions as possible. Because drawBoard does one thing, it doesn't interfere with anything else in the game. If no win condition is found, we switch the current active player and removed the click event so that no player could click on that particular box again. Here is the function that checks for a winning combination. It checks the players array and compares it with the list of predefined winners.

function checkWinner() {
    // check if current player has a winning hand
    // only start checking when player x has size number of selections
    var win = false;
    var playerSelections = new Array();

    if (currentPlayer == 0)
        playerSelections = player1Selections;
    else
	playerSelections = player2Selections;
    
    if (playerSelections.length >= size) {
        // check if any 'winners' are also in your selections
        
        for (i = 0; i < winners.length; i++) {
            var sets = winners[i];  // winning hand
            var setFound = true;
            
            for (r = 0; r < sets.length; r++) {
                // check if number is in current players hand
                // if not, break, not winner
                var found = false;
                
                // players hand
                for (s = 0; s < playerSelections.length; s++) {
                    if (sets[r] == playerSelections[s]) {
                        found = true;
                        break;
                    }
                }

                // value not found in players hand
                // not a valid set, move on
                if (found == false) {
                    setFound = false;
                    break;
                }
            }

            if (setFound == true) {
                win = true;
                break;
            }
        }
    }

    return win;
} 

Detecting a Draw

Last on the list is detecting if we have a draw. That is, if no winning rows have been detected. And we can do that simply by checking the number of selections that the players have made. If we see that we have reached (n * n) selections, then we can assume that we have taken the board completely. We can add this check after each user plays their turn, by adding a conditional statement before control switches over to the next player.

if (checkWinner())
{
      if(currentPlayer == 0)
          points1++;
      else
          points2++;

      document.getElementById("player1").innerHTML = points1;
      document.getElementById("player2").innerHTML = points2;

      reset();
      drawBoard();
 }

else if (player2Selections.length + player1Selections.length == 9)
 {
     reset();
     drawBoard();
}

Reset

And lastly, once the current game is over, we want to go ahead and reset the screen and its elements.

function reset()
{
    currentPlayer = 0;
    player1Selections = new Array();
    player2Selections = new Array();
    d('player1').classList.add('selected');
    d('player2').classList.remove('selected');
}

The full source code can be found down below. There's always room for improvement, and new features can always be added. If you like the code and use it somewhere online, feel free to link back to me and let me know. Not required, but it is always appreciated.

Full Source
<!DOCTYPE html>
<!--[if lt IE 7]>      <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
<!--[if IE 7]>         <html class="no-js lt-ie9 lt-ie8"> <![endif]-->
<!--[if IE 8]>         <html class="no-js lt-ie9"> <![endif]-->
<!--[if gt IE 8]><!--> <html> <!--<![endif]-->
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <title></title>
        <meta name="description" content="">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <link rel="stylesheet" href="tictactoe.css">
    </head>
    <body>
        <!--[if lt IE 7]>
            <p class="browsehappy">You are using an <strong>outdated</strong> browser. Please <a href="#">upgrade your browser</a> to improve your experience.</p>
        <![endif]-->
        <div class="game">
            <div class="game-title" contenteditable="true">Tic Tac Toe</div>
            <div class="player" contenteditable="true">
            Player 1
            <div style="font-size:30pt;" id="player1" class="selected" contenteditable="true">
            0
            </div>
            </div>
            
            <table id="game" class="tictactoe" style="float:left;width:45%;" contenteditable="true">
            </table>
            
            <div class="player" contenteditable="true">
            Player 2
            <div id="player2" style="font-size:30pt;" contenteditable="true">0</div>
            </div>
            
            <div class="clear" contenteditable="true"></div>
            </div>
        <script src="tictactoe.js" async defer></script>
    </body>
</html>
body{
    margin:0px;
    padding:0px;
}

.game td
{
    width:100px;
    height:100px;
    border: solid 5px black;
    text-align:center;
    font-size:40pt;
    border-radius:10px;
    box-sizing:border-box;
}

.game td:hover
{
    background-color: #fff;
    cursor:pointer;
}

.game
{
    background:#efefef;
    border:solid 10px #333;
}

.game .game-title
{
    background:#222;
    color:#fff;
    padding: 10px;
    text-align:center;
}

.game .player
{
    float:left;
    text-align:center;
    width: 25%;
}

.game .player .selected
{
    border-bottom:solid 4px #00fa9a;
}

.tictactoe-board
{
    text-align:center;
    margin:0 auto;
    width:50%;
    padding-top:20px;
}

.tictactoe-board table
{
    float:left;
    width: 50%;
}

.tictactoe-board-score1
{
    float:left;
    width:20%;
}

.tictactoe-board-score2
{
    float:left;
    width:20%;
}

.clear
{
    clear:both;
}
var winners = new Array();
var player1Selections = new Array();
var player2Selections = new Array();
var timer;
var numberOfPlayers = 2;
var currentPlayer = 0;
var move = 0;
var points1 = 0;    // player 1 points
var points2 = 0;    // player 2 points
var size = 3;

function drawBoard() {
    var Parent = document.getElementById("game");
    var counter = 1;
    
    while (Parent.hasChildNodes()) {
        Parent.removeChild(Parent.firstChild);
    }

    for (s = 0; s < 3; s++) {
        var row = document.createElement("tr");
        
        for (r = 0; r < 3; r++) {
            var col = document.createElement("td");
            col.id = counter;

            var handler = function(e) {
                if (currentPlayer == 0) {
                    this.innerHTML = "X";
                    player1Selections.push(parseInt(this.id));
                    player1Selections.sort(function(a, b) { return a - b });
                    d('player1').classList.remove('selected');
                    d('player2').classList.add('selected');
                }

                else {
                    this.innerHTML = "O";
                    player2Selections.push(parseInt(this.id));
                    player2Selections.sort(function(a, b) { return a - b });
                    d('player1').classList.add('selected');
                    d('player2').classList.remove('selected');
                }

                if (checkWinner())
                {
                    if(currentPlayer == 0)
                        points1++;
                    else
                        points2++;

                    document.getElementById("player1").innerHTML = points1;
                    document.getElementById("player2").innerHTML = points2;

                    reset();
                    drawBoard();
                }

                else if (player2Selections.length + player1Selections.length == 9)
                {
                    reset();
                    drawBoard();
                }
                else
                {
                    if (currentPlayer == 0)
                        currentPlayer = 1;
                    else
                        currentPlayer = 0;
                    this.removeEventListener('click', arguments.callee);
                }
            };

            col.addEventListener('click', handler);

            row.appendChild(col);
            counter++;
        }

        Parent.appendChild(row);
    }

    loadAnswers();
}

function d(id)
{
    var el = document.getElementById(id);
    return el;
}
function reset()
{
    currentPlayer = 0;
    player1Selections = new Array();
    player2Selections = new Array();
    d('player1').classList.add('selected');
    d('player2').classList.remove('selected');
}

function loadAnswers()
{
    winners.push([1, 2, 3]);
    winners.push([4, 5, 6]);
    winners.push([7, 8, 9]);
    winners.push([1, 4, 7]);
    winners.push([2, 5, 8]);
    winners.push([3, 6, 9]);
    winners.push([1, 5, 9]);
    winners.push([3, 5, 7]);
}

function checkWinner() {
    // check if current player has a winning hand
    // only stsrt checking when player x has size number of selections
    var win = false;
    var playerSelections = new Array();

    if (currentPlayer == 0)
        playerSelections = player1Selections;
    else
	playerSelections = player2Selections;
    
    if (playerSelections.length >= size) {
        // check if any 'winners' are also in your selections
        
        for (i = 0; i < winners.length; i++) {
            var sets = winners[i];  // winning hand
            var setFound = true;
            
            for (r = 0; r < sets.length; r++) {
                // check if number is in current players hand
                // if not, break, not winner
                var found = false;
                
                // players hand
                for (s = 0; s < playerSelections.length; s++) {
                    if (sets[r] == playerSelections[s]) {
                        found = true;
                        break;
                    }
                }

                // value not found in players hand
                // not a valid set, move on
                if (found == false) {
                    setFound = false;
                    break;
                }
            }

            if (setFound == true) {
                win = true;
                break;
            }
        }
    }

    return win;
} 

window.addEventListener('load', drawBoard);
Walter G. author of blog post
Walter Guevara is a Computer Scientist, software engineer, startup founder and previous mentor for a coding bootcamp. He has been creating software for the past 20 years.

Get the latest programming news directly in your inbox!

Have a question on this article?

You can leave me a question on this particular article (or any other really).

Ask a question

Community Comments

k
keitumetse
9/7/2017 3:03:51 AM
how do i run these codes,do i link them to an html file or what im confused
thatsoftwaredude.com logo
Walt
9/8/2017 10:13:22 AM
If you copy and paste the full source into an html file, it will run. However, I will add this code to CodePen and include it on this post. Thanks for your comment!
k
kehlyn
9/27/2017 4:57:33 AM
can you make a code to show if the game has ended in a draw
thatsoftwaredude.com logo
Walt
9/27/2017 11:59:52 AM
Absolutely can :) The post has been updated and the sample code too!
B
Bruno
3/30/2019 4:49:20 AM
Pls is there any code to indicate that the game has finished or draw and refreshs the page
B
Bruno Ezemba austine
3/31/2019 3:41:36 AM
Pls if you can, let me Kno w
k
kehlyn
9/27/2017 1:19:13 PM
thanks for the code
thatsoftwaredude.com logo
Walt
9/28/2017 10:47:24 AM
You are very much welcome!
R
Robert
9/29/2017 4:00:43 AM
Hey, thanks for your tutorial. Could you please explain how can I remove the numbers from the table? Appreciate that.
thatsoftwaredude.com logo
Walt
9/29/2017 7:29:38 PM
You are very welcome! And absolutely. In the drawBoard() function, simply remove the following line:

col.innerHTML = counter;

And that should do it!
J
Jonno
9/20/2018 2:14:42 AM
Hi bro, could you please explain how to make players choose whether they want to be player 1 or player 2? I'm really struggling. Please assist
thatsoftwaredude.com logo
Walt
11/4/2018 10:11:43 PM
Well, player 1 is whoever goes first really.
H
Hambaba
3/29/2019 3:33:22 AM
Very nice work, do you know how to remove the tiny spacing between the table? That really bothers me
H
Hambaba
3/29/2019 3:44:48 AM
Nvm, I've found the css property of border-collapse, thanks anyway
T
Tom
4/6/2019 8:09:41 AM
Hi Walt, May I ask why you chose to initialise the arrays using new Array() rather than the array literal? E.g., player1Selections, as player1Selections = new Array(); rather than player1Selections = []; . I am relatively new to JS and wondered about your style choice. Thank you very much for the tutorial.
thatsoftwaredude.com logo
Walt
4/8/2019 8:18:58 AM
Hey there Tom. Many thanks for checking out the post and the question. It's a definitely a personal choice lol I am a C# developer as well, so am very used to initializing objects using their given constructors. But for sure both methods are perfectly fine.
I
Irene Luckyj
10/21/2019 5:52:41 PM
Hello, Really great code! I was wondering if you would add code that would allow the user to play against the computer. I.e. a one player game where the program chooses random cell blocks on the game. Like AI. Thank you so much!
thatsoftwaredude.com logo
Walt
10/21/2019 7:30:45 PM
Hey there Irene, many thanks for the kind words. That's a fantastic idea. Adding it to my queue and will see what I can do for sure.
I
Irene Luckyj
10/22/2019 7:58:52 AM
Hey! Another question, for the code player1Selections.push(parseInt(this.id)); player1Selections.sort(function(a, b) { return a - b }); d('player1').classList.remove('selected'); d('player2').classList.add('selected'); are you using this to switch the x and o or this code to place the x or o into the actual cell block? I am unfamiliar with using handlers, so I was unsure what was exactly happening here. thank you for your quick response!
thatsoftwaredude.com logo
Walt
10/22/2019 12:33:34 PM
The first line actually adds the selected box index to the player1Selections array (from 1 - 9) and then sorts that list upwards. For example, if player has the first and third box selected, that array would be [1, 3]. d('player1').classList.remove('selected'); d('player2').classList.add('selected'); These just clear the bar that appears under the currently active player. And you are very much welcome.
I
Irene Luckyj
10/22/2019 1:09:18 PM
I'm trying to build a one player method for this game. I'm the most confused about how I manually set the innerHTML value for a cell without the cell being clicked. I.e. for when the computer randomly generates a cell id how do I add 'O' to that cell rather than using the event listener. Thank you again!!!
i
irene luckyj
10/22/2019 1:10:22 PM
would you do this in the drawBoard() or an entirely different function?
thatsoftwaredude.com logo
Walt
10/22/2019 2:55:51 PM
You would probably want to do it right after the manual player (yourself) makes a click. Right after, you could run a function to automatically/randomly make the next click as player 2. You would want to write a function that would mimic whatever a player would do.
i
irene
10/22/2019 7:46:24 PM
would you do this inside the event handler function? as in after you do the if (currentPlayer == 0) test maybe like a if (computerPlayMode) test and then under that write the code? or would u do it outside of the event handler?
thatsoftwaredude.com logo
Walt
10/23/2019 10:11:25 AM
Right. That would be the best place. As soon as the user makes their selection, you immediately want the computer to make its move. You might want to add a delay so that it looks like it's thinking :)
L
12/25/2019 3:01:36 AM
Hi Walter - Thanks for posting this. It is really helpful. I'm new to coding and had two questions: 1) In the handler function, what does "this" refer to in terms of this.innerHTML and this.id? 2) Are the for(s=0...) and for(r=0...) loops creating the tic tac toe grid? If not, what are they for?
thatsoftwaredude.com logo
Walt
12/31/2019 12:21:11 PM
Hey there Lance, Thank you for kind words and the question. For your first question, "this" refers to the calling element of the event handler. If you notice below that line, I have the following: col.addEventListener('click', handler); So 'this' would refer to the col object in this case. And for your 2nd question, yes indeed, the first loop creates the rows, and the second one creates the columns for each of the rows! If there's anything else, feel free to let me know!
D
Darshan
6/3/2020 9:44:26 PM
Thanks a Lot Walt.
thatsoftwaredude.com logo
Walter
6/4/2020 4:51:45 PM
You are very welcome Darshan. ??
o
9/12/2020 2:14:58 PM
thanks
thatsoftwaredude.com logo
Walter
3/1/2021 10:39:23 PM
Ey, you are very much welcome.
z
zk
6/3/2021 8:58:05 AM
Sir, may I know why I can't run the code on vscode
z
zk
6/3/2021 8:58:09 AM
Sir, may I know why I can't run the code on vscode
J
Jimmy
6/17/2021 10:52:53 AM
Hey Walter, I have a question, may I know how those 3 for loops works in checkWinner() function? Thank you so much.

Add a comment