x
Tic-Tac-Toe with Javascript ES2015

Part 2: Building the Tic-Tac-Toe Board

This is a part of the series: Tic-Tac-Toe with Javascript ES2015

In this part, we will start building the logic behind the Tic-Tac-Toe board. We will learn how to create a Javascript class that represents the board. This class will hold the current state of the board in addition to methods that get some info about the board.

View a demo or visit the project’s github page.
The Board Structure

The only argument for our board class will be an array of length 9. This array will hold the state of the board. The state refers to the current configuration of the board or the positions of X’s and O’s. Each index in the array will refer to a certain cell in the board. If we define the state as [‘x’,”,’o’,’o’,”,”,’x’,”,”] it will map to this:

Let’s create a new folder inside our src directory and call it classes. Inside classes folder, create a new file called Board.js. In this file we will define our board class:

class Board {
	constructor(state = ['','','','','','','','','']) {
        this.state = state;
    }
}

export default Board;

Remember our entry point where webpack is going to look at is src/index.js. So in this file we will import all of our classes and any other imports. To import our board class, we use an import statement with a relative path:

import Board from './classes/Board';

 

Printing a Formatted Board

The first method we are going to create is not necessary for the game’s logic; however, it’s going to help us visualize the board in the browser’s console as we develop. This method is going to be called printFormattedBoard:

printFormattedBoard() {
	let formattedString = '';
    this.state.forEach((cell, index) => {
    	formattedString += cell ? ` ${cell} |` : '   |';
    	if((index + 1) % 3 == 0)  {
    		formattedString = formattedString.slice(0,-1);
    		if(index < 8) formattedString += '\n\u2015\u2015\u2015 \u2015\u2015\u2015 \u2015\u2015\u2015\n';
    	}
	});
	console.log('%c' + formattedString, 'color: #6d4e42;font-size:16px');
}

This methods iterates the state array using forEach, and prints each cell content + a vertical line next to it. Every 3 cells, we print 3 horizontal lines using \u2015 unicode charachter in a new line. We also make sure that we don’t print the 3 horizontal lines after the last 3 cells. To test this, in index.js type:

import Board from './classes/Board';

let board = new Board(['x','o','','','o','','','','o']);
board.printFormattedBoard();

 

Checking the Board’s Status

The next 3 methods will be used to check the current status of the board. We need to check for 3 things; is the board empty? is the board full? and is the board in a terminal state? Terminal state is where either one of the players won or the game is a draw.

To check if the board is empty, we will use ES2015 array helper every.

isEmpty() {
    return this.state.every(function(cell) { 
        return cell == ''; 
    });
}

The every helper will return true if every iteration returned true; i.e. if cell == '' is true for all cells. cell == '' can be refactored to !cell since an empty string is a false statement. Also, we can use arrow functions instead of normal functions. Thus, isEmpty and isFull can be as so:

isEmpty() {
    return this.state.every(cell => !cell);
}
isFull() {
    return this.state.every(cell => cell);
}

The last thing we need to check is the terminal state board. This is method is going to be long but very systematic and repetitive. First we will use isEmpty and return false if the board is empty. Then using if conditions we will check for horizontal, vertical and diagonal wins. If non of the conditions is true, we will check if the board is full. If the board is full and none of the winning conditions is met, then it must be a draw.

In case a win or a draw happen, an object will be returned containing the winner, the direction of winning and the row/column number where he won. This object will be very useful when we build our UI for the game.

isTerminal() {
	//Return False if board in empty
    if(this.isEmpty()) return false;

    //Checking Horizontal Wins
    if(this.state[0] == this.state[1] && this.state[0] == this.state[2] && this.state[0]) {
    	return {'winner': this.state[0], 'direction': 'H', 'row': 1};
    }
    if(this.state[3] == this.state[4] && this.state[3] == this.state[5] && this.state[3]) {
    	return {'winner': this.state[3], 'direction': 'H', 'row': 2};
    }
    if(this.state[6] == this.state[7] && this.state[6] == this.state[8] && this.state[6]) {
    	return {'winner': this.state[6], 'direction': 'H', 'row': 3};
    }

    //Checking Vertical Wins
    if(this.state[0] == this.state[3] && this.state[0] == this.state[6] && this.state[0]) {
    	return {'winner': this.state[0], 'direction': 'V', 'row': 1};
    }
    if(this.state[1] == this.state[4] && this.state[1] == this.state[7] && this.state[1]) {
    	return {'winner': this.state[1], 'direction': 'V', 'row': 2};
    }
    if(this.state[2] == this.state[5] && this.state[2] == this.state[8] && this.state[2]) {
    	return {'winner': this.state[2], 'direction': 'V', 'row': 3};
    }

    //Checking Diagonal Wins
    if(this.state[0] == this.state[4] && this.state[0] == this.state[8] && this.state[0]) {
    	return {'winner': this.state[0], 'direction': 'D', 'row': 1};
    }
    if(this.state[2] == this.state[4] && this.state[2] == this.state[6] && this.state[2]) {
    	return {'winner': this.state[2], 'direction': 'D', 'row': 2};
    }

    //If no winner but the board is full, then it's a draw
    if(this.isFull()) {
        return {'winner': 'draw'};
    }
    
    //return false otherwise
    return false;
}

Let’s test this in our index.js:

import Board from './classes/Board';

let board = new Board(['x','o','x','x','o','o','o','o','x']);
board.printFormattedBoard();
console.log(board.isEmpty());
console.log(board.isFull());
console.log(board.isTerminal());

 

Inserting a Symbol and Getting Moves

Insert method will simply insert a symbol at a certain cell. The method will receive the symbol and the position. We will check first if the cell is occupied or does not exist. Otherwise we will simply update the state array:

insert(symbol, position) {
    if(position > 8 || this.state[position]) return false; //Cell is either occupied or does not exist
    this.state[position] = symbol;
    return true;
}

Finally, we will create a method that returns an array containing all available moves. This will simply iterate the state array and pushes to the returned array the index of the cell only if it’s empty:

getAvailableMoves() {
    const moves = [];
    this.state.forEach((cell, index) => {
        if(!cell) moves.push(index); 
    });
    return moves;
}

Let’s now test these 2 methods. In index.js type:

import Board from './classes/Board';

let board = new Board(['x','o','','x','o','','o','','x']);
board.printFormattedBoard();
console.log(board.insert('x',2));
board.printFormattedBoard();
console.log(board.getAvailableMoves());

Here is how the final board class should look:

/**
  * @desc This class represents the board, contains methods that checks board state, insert a symbol, etc..
  * @param {Array} state - an array representing the state of the board
*/
class Board {
    //Initializing the board
    constructor(state = ['','','','','','','','','']) {
        this.state = state;
    }
    //Logs a visualised board with the current state to the console
    printFormattedBoard() {
    	let formattedString = '';
        this.state.forEach((cell, index) => {
        	formattedString += cell ? ` ${cell} |` : '   |';
        	if((index + 1) % 3 == 0)  {
        		formattedString = formattedString.slice(0,-1);
        		if(index < 8) formattedString += '\n\u2015\u2015\u2015 \u2015\u2015\u2015 \u2015\u2015\u2015\n';
        	}
		});
		console.log('%c' + formattedString, 'color: #6d4e42;font-size:16px');
    }
    //Checks if board has no symbols yet
    isEmpty() {
        return this.state.every(cell => !cell);
    }
    //Check if board has no spaces available
    isFull() {
        return this.state.every(cell => cell);
    }
    /**
     * Inserts a new symbol(x,o) into
     * @param {String} symbol 
     * @param {Number} position
     * @return {Boolean} boolean represent success of the operation
     */
    insert(symbol, position) {
    	if(position > 8 || this.state[position]) return false; //Cell is either occupied or does not exist
    	this.state[position] = symbol;
    	return true;
    }
    //Returns an array containing available moves for the current state
    getAvailableMoves() {
        const moves = [];
        this.state.forEach((cell, index) => {
            if(!cell) moves.push(index); 
        });
        return moves;
    }
    /**
     * Checks if the board has a terminal state ie. a player wins or the board is full with no winner
     * @return {Object} an object containing the winner, direction of winning and row number
     */
    isTerminal() {
    	//Return False if board in empty
        if(this.isEmpty()) return false;

        //Checking Horizontal Wins
        if(this.state[0] == this.state[1] && this.state[0] == this.state[2] && this.state[0]) {
        	return {'winner': this.state[0], 'direction': 'H', 'row': 1};
        }
        if(this.state[3] == this.state[4] && this.state[3] == this.state[5] && this.state[3]) {
        	return {'winner': this.state[3], 'direction': 'H', 'row': 2};
        }
        if(this.state[6] == this.state[7] && this.state[6] == this.state[8] && this.state[6]) {
        	return {'winner': this.state[6], 'direction': 'H', 'row': 3};
        }

        //Checking Vertical Wins
        if(this.state[0] == this.state[3] && this.state[0] == this.state[6] && this.state[0]) {
        	return {'winner': this.state[0], 'direction': 'V', 'row': 1};
        }
        if(this.state[1] == this.state[4] && this.state[1] == this.state[7] && this.state[1]) {
        	return {'winner': this.state[1], 'direction': 'V', 'row': 2};
        }
        if(this.state[2] == this.state[5] && this.state[2] == this.state[8] && this.state[2]) {
        	return {'winner': this.state[2], 'direction': 'V', 'row': 3};
        }

        //Checking Diagonal Wins
        if(this.state[0] == this.state[4] && this.state[0] == this.state[8] && this.state[0]) {
        	return {'winner': this.state[0], 'direction': 'D', 'row': 1};
        }
        if(this.state[2] == this.state[4] && this.state[2] == this.state[6] && this.state[2]) {
        	return {'winner': this.state[2], 'direction': 'D', 'row': 2};
        }

        //If no winner but the board is full, then it's a draw
        if(this.isFull()) {
            return {'winner': 'draw'};
        }
        
        //return false otherwise
        return false;
    }
}

export default Board;

In the next part, we will start creating a Player class. This class will use an algorithm to get the best possible move. We will also add different difficulty levels to this player.

Ali Alaa

Front-end developer from Egypt. Telecommunications Engineering graduate. Been working in web development since graduation.