Coding a Tic-Tac-Toe game

Rossella Ferrandino
9 min readFeb 18, 2019

--

As part of my very first interview for a web developer job, I was asked to code a Tic-Tac-Toe game. Funny story, I actually had to google what a Tic-Tac-Toe game is, as in Italy we know this as “Tris”. Anyway, what I thought was a simple game turned out to be very complex behind the scenes. Since coding this game is also part of the challenges of the FreeCodeCamp curriculum, I decided to write down my thought process as I am completing this project.

The basic HTML layout

First of all, we need the classic game grid. I made this with 9 divs which all have the class of block and which borders are styled according to their position in the page. I learned a new trick, :nth-child(3n+0) which allows you to select for example all of the elements whose index is a multiple of 3. The number that is added to the multiples indicates the offset, so in my example I would only want the multiple of 3.

Recognising the player’s move

The second step is to handle the player’s selection. Since I have assigned the class “block” to all of my divs, using a for loop I will add a “click” event listener to the individual div to understand which one was clicked. If the user clicks on a specific div, the “clicked” CSS class will be added to the div and this will in turn display a cross icon. This cross icon is part of the div in the HTML already, however it is hidden by default and displayed as block only when clicked.

function player() {
for (let i = 0; i < blocks.length; i++) {
blocks[i].addEventListener(‘click’, function() {
this.getElementsByTagName(‘i’)[0].classList.add(‘clicked’);
})
}
}

If I wanted to give the players the option to choose whether they want to play as X or O, I would add an if statement that adds the correct Fontawesome icon. [I haven’t added this functionality to my current project due to time constraints but I will work on this and update the final code]

function player(symbol) {
for (let i = 0; i < blocks.length; i++) {
blocks[i].addEventListener(‘click’, function() {
if(symbol === ‘cross’) {
this.getElementsByTagName(‘i’)[0].classList.add(‘clicked’);
}
else if (symbol === ‘circle’) {
this.getElementsByTagName(‘i’)[1].classList.add(‘clicked’);
}
})
}
}

Since I am now able to add a cross to the game board, I also want the option to manually reset the game. I add a reset button to the HTML and in my JavaScript with a “click” event listener, I remove the “clicked” class I have added with the player function.

Player and computer moves

It’s time to work on the logic behind the game. How do we keep track of where the player (and the computer) have added their symbols and how do we know if someone has won? I am going to use the divs’ ids and an array of the winning combinations to check whether the winning blocks of the grid have been selected and by which player.

This is where the tricky part comes. We want the player to be able to make their move by clicking on the grid, while the computer will randomly choose the block id and place its icon. This means that we need two different functions. For the player, the function will loop through the divs and with an event listener, add the class of ‘clicked’ to the grid selected.

let playerMoves = [];function player() {//adding a cross to the clicked blocks
for (let i = 0; i < blocks.length; i++) {
blocks[i].addEventListener("click", function() {
if (computerMoves.indexOf(Number(this.id)) !== -1 ||
playerMoves.indexOf(Number(this.id)) !== -1) {
return;
}
this.getElementsByTagName("i")[0].classList.add("clicked");
//storing the moves from the player to compare with winning resultplayerMoves.push(Number(this.id));
});
}
}

To avoid duplicate moves, I have added an if statement that checks whether that specific div id is already in our playerMoves or computerMoves array. It will return if this is the case.

The computer moves are randomised using the helper function pickRandomBlock, which picks a random number between 1 and 9 and then returns the specific block selected. There will be some if statements within the computer function, with the aim of checking whether the block id already is part of the computer or the player moves arrays, as with our player function. If the id of the random div picked by the pickRandomBlock is a duplicate, the computer function will be run again.

function pickRandomBlock() {
let random = Math.floor(Math.random() * blocks.length);
return blocks[random];
}
//computer actions
function computer() {
let random = pickRandomBlock();
for (let i = 0; i < blocks.length; i++) {
if (
computerMoves.indexOf(Number(this.id)) !== -1 ||
playerMoves.indexOf(Number(this.id)) !== -1
) {
return computer();
}
random.getElementsByTagName("i")[1].classList.add("clicked");
}
//add the computer move to the moves array
if (!computerMoves.includes(Number(this.id)) {
computerMoves.push(Number(this.id));
}
}

Now we need two more functions that determine the logic behind the game: one will be aimed at checking if one of the players has won and the second one will determine whose turn it is to play.

Who has won?

At the beginning of our code, we have declared a nested array that contains the winning combinations of div ids. My first thought was to compare the playerMoves and the computerMoves to the elements of the winning array and reset the game if someone has won. However, this solution only works if the player or the computer make exactly three winning moves, which is not exactly what happens in reality.

So what we need to be able to do is check whether the playerMoves or computerMoves arrays contain all three of the values of one of the winning combinations. In order to do this, we use a combination of filter and indexOf array methods. The filter() method creates a new array with all elements that pass the test implemented by the provided function. Firstly, we filter those elements from the winningCombinations sub-arrays that appear in the moves array, and then we filter the result again to ensure that they all appear in the moves array. Then we store this result array in a new variable — if the foundResults variable has any elements in it, it means that one of the players has won. For the moment, we will console.log() a message and reset the board if this is the case.

const winningCombinations = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[2, 5, 8],
[0, 3, 6],
[1, 4, 7],
[0, 4, 8],
[2, 4, 6]
];
let playerMoves = [];
let computerMoves = [];
function hasWon(moves, winningCombinations) {
let foundResults = winningCombinations.filter(
array =>
array.filter(item => {
return moves.indexOf(item) > -1;
}).length === 3
);
if (foundResults.length > 0) {
if (whoseTurn === "computer") {
displayResult("You won");
} else if (whoseTurn === "player") {
displayResult("The computer has won");
}
didSomeoneWin = true;
}
}

Whose turn is it?

The final piece of the puzzle is to determine whether the player or the computer will play next. Firstly, we need some variables to be declared in the global scope:

  • totalMoves — keeps track of both the computer and player moves
  • whoseTurn —it will change to ‘player’ or ‘computer’
  • hasSomeoneWon — boolean

What I have done is creating a new function that will be run within the computer() and player() functions and that switches turn, adds 1 to the totalMoves variables, check if one of the two players has won and plays the next turn of the game.

function nextTurn(opponent, whoseMoves) {
whoseTurn = opponent;
totalMoves++;
hasWon(whoseMoves, winningCombinations);
playGame();
}

I also created the function playGame, which will run as soon as the page loads. This function has the role of checking if there is a winner or if there is a tie and display the correct message on the overlay, or to run either the computer() or player() function according to whose turn it is.

window.onload = playGame();function playGame() {
if (totalMoves === 9 && !didSomeoneWin) {
console.log('It's a tie');
} else if (didSomeoneWin) {
setTimeout(function() {
reset();
}, 1000);
}
//the player always starts
else if (whoseTurn === "player" || totalMoves === 0) {
player();
} else if (whoseTurn === "computer") {
setTimeout(function() {
computer();
}, 200);
}
}

Displaying the winning player

I have decided to display the winner by adding an overlay with some text. In order to do so, I need to create a function that displays the overlay and then re-factor my hasWon function to run when either the computer or the player wins, as well as my playGame function to display the appropriate message in case of a tie.

function displayResult(winningMessage) {
document.getElementById("overlay").style.display = "block";
document.getElementById("text").textContent = winningMessage;
let resetButton = document.createElement("button");
resetButton.classList.add("overlayResetButton");
resetButton.textContent = "Reset Game";
resetButton.addEventListener("click", function() {
reset();
});
document.getElementById("text").appendChild(resetButton);
}

This function also includes a reset button, which I have decided not to use in my codepen.

Extra: Making the computer smarter

With a simple random selection, the computer moves don’t really stop you from winning. This is why we need some more work on the computer() function. I created a helper function called smartComputer(). This is run at the beginning of the computer() function and it checks if the player moves array includes two of the numbers of one of the winningCombinations array. This has basically the same logic of the hasWon() function and it will store the array of potential winning combinations for the player in the playerPotentialWins array. If this array has any item in it, the smartComputer() function will look up which number of that nested array hasn’t been selected yet and assign it to the global variable smartComputerNextMove. It will also mark the global variable playerAboutToWin as true so that our pickRandomBlock() function will select the next move accordingly.

//returns the winning move for the player and assigns this to the smartComputerNext move variable
function smartComputer() {
let playerPotentialWins = winningCombinations.filter(
array =>
array.filter(item => {
return playerMoves.indexOf(item) > -1;
}).length === 2
);
if (playerPotentialWins.length > 0) {
playerAboutToWin = true;
playerPotentialWins.filter(array =>
//get the index of the next computer move
array.filter(item => {
if (
playerPotentialWins.indexOf(item) === -1 &&
playerMoves.indexOf(item) === -1 &&
computerMoves.indexOf(item) === -1
) {
smartComputerNextMove = item;
}
})
);
}
}

New logic of the pickRandomBlock() function:

//pick random location for computer move
function pickRandomBlock() {
//picks location for computer move
let random;
if (playerAboutToWin) {
if (
computerMoves.indexOf(smartComputerNextMove) === -1 &&
playerMoves.indexOf(smartComputerNextMove) === -1
) {
random = smartComputerNextMove;
playerAboutToWin = false;
} else {
random = Math.floor(Math.random() * blocks.length);
}
}
//handling tie
else if (totalMoves === 9 && didSomeoneWin === false) {
return;
} else {
random = Math.floor(Math.random() * blocks.length);
}
return blocks[random];
}

This is where my project is at the current stage, however I will work on it some more to add the following functionality:

  • Smarter computer: at the moment the computer stops the player from winning, however we want it to also look up its own potential wins and make a move on the correct div if the player is not about to win yet
  • Who plays first: we could add two buttons to give the player the option to play first or to play after the computer made its first move. We would need to listen for a click on the two buttons with the same class and then amend the playGame() function to declare whose turn it is when the page loads.
  • Choose your own icon: since I used Fontawesome to get the cross and circle icons, it would be a fun idea to allow the user to click a button to pick a random icon from an object declared in our JS. You could do a game with dog VS cats!

The end…

…for now! I will update the article or write a sequel when I have implemented the additional functionality I just talked about.

Thanks for reading! If you liked this story, please recommend it by clapping or sharing on social media.

Current version of the project on my codepen.

--

--

Rossella Ferrandino

Front end developer and co-owner of Nama Studio, Shopify agency in Italy