Source: board.js

/**
 * A basic board, the first-step for using the engine
 * @constructor
 */
class Board {
    constructor() {
        /**
         * The corresponding turn, 0 means O and 1 means X
         * Automatically re-assigned, don't move the value by your own
         * @private
         * @type {number}
         */
        this._turn = 1

        /**
         * The data shown in arrays.
         * 0 means O and 1 means X.
         * @private
         */
        this.data = [
            [null, null, null],
            [null, null, null],
            [null, null, null]
        ]

        /**
         * If the game is over
         * @type {Boolean}
         */
        this.gameOver = false
    }

    /**
     * Makes a move on the board.
     * @param {number} player The player that moves. 0 means O and 1 means X
     * @param {number} cell The cell, cells are numbered from 1-9.
     * @param {Boolean} check Check at the end of the turn if the game is over (recommended and by default). If not true, then you should check it manually via checkGameOver() at the end of the turn:
     */
    makeMove(player, cell, check = true) {
        if (this.gameOver) throw new InvalidMovement("Game is already over.")

        if (player !== 0 && player !== 1) throw new InvalidMovement("The player is neither 0 (O) or 1 (X).")
        if (cell < 1 || cell > 9) throw new InvalidMovement("Invalid cell. Cells are numbered from 1 to 9.")

        //console.debug(`${player} played on ${cell}`)
        if (player !== this._turn) throw new InvalidMovement(`Is not the turn of that player. Got ${player}, expected ${this.__turn}`)

        const parsedCell = this._parseCell(cell)
        if (typeof parsedCell == "number") throw new InvalidMovement("This cell is already occupied.")

        this.data[parsedCell.row][parsedCell.col] = this._turn // set value

        const nextTurn = this._turn == 0 ? 1 : 0
        this._turn = nextTurn// next turn

        check ? this.checkGameOver() : null
    }

    /**
     * Checks if the game is over (tie, win by row, by column or by diagonal).
     * Returns "tie" if there's a tie, the winner or null if the game can continue.
     * @returns {number|string|null}
     */
    checkGameOver() {
        const checkRow = (row) => {
            const first = this.data[row][0]
            if (first === null) {
                return null
            }
            for (let i = 1; i < 3; i++) {
                if (this.data[row][i] !== first) {
                    return null
                }
            }
            return first
        }
        const checkColumn = (col) => {
            const first = this.data[0][col]
            if (first === null) {
                return null
            }
            for (let i = 1; i < 3; i++) {
                if (this.data[i][col] !== first) {
                    return null
                }
            }
            return first
        }
        const checkDiagonal = () => {
            const center = this.data[1][1]
            if (center === null) {
                return null
            }
            if (this.data[0][0] === center && center === this.data[2][2]) {
                return center
            }
            if (this.data[0][2] === center && center === this.data[2][0]) {
                return center
            }
            return null
        }
        // check rows
        for (let row = 0; row < 3; row++) {
            const winner = checkRow(row)
            if (winner !== null) {
                this.gameOver = true
                return winner
            }
        }
        // check columns
        for (let col = 0; col < 3; col++) {
            const winner = checkColumn(col)
            if (winner !== null) {
                this.gameOver = true
                return winner
            }
        }
        // check diagonal
        const winner = checkDiagonal()
        if (winner !== null) {
            this.gameOver = true
            return winner
        }
        // check for tie
        for (let row = 0; row < 3; row++) {
            for (let col = 0; col < 3; col++) {
                if (this.data[row][col] === null) {
                    // there is at least one empty cell, the game is not over
                    return null
                }
            }
        }
        // all cells are occupied, it's a tie
        this.gameOver = true
        return "tie"
    }

    /**
     * Parses a cell from 1-9 to actual board data.
     * Used internally, do not pass it to cell from makeMove.
     * @param {number} cell The cell from 1-9.
     * @private
     */
    _parseCell(cell) {
        let row = 0
        let col = 0

        if (cell <= 3) row = 0
        else if (cell <= 6) row = 1
        else row = 2

        if (cell <= 3) {
            row = 0;
            col = cell - 1;
        } else if (cell <= 6) {
            row = 1;
            col = cell - 4;
        } else {
            row = 2;
            col = cell - 7;
        }

        // console.debug(`Parsed cell ${cell} as row ${row} col ${col}`)

        return {
            row,
            col,
            value: this.data[row][col]
        }
    }
}

class InvalidMovement extends Error {
    constructor() {
        super()
        this.name = "InvalidMovementError"
    }
}

module.exports = Board;