0
\$\begingroup\$

The working page is in GitHub.

Introduction

This time, I have two AI bots playing Connect Four against each other. The first AI bot uses Alpha-beta pruning, and the other one uses a Negamax with Alpha-beta pruning.

const ROWS = 6;
const COLUMNS = 7;
const MARGIN_RADIUS = 10;
const PROGRESS_BAR_HEIGHT = 15;
const O = 0;
const X = 1;
const EMPTY = -1;
const BOARD_FILL_STYLE = '#3879e0';

class ConnectFourHeuristicFunction {
    static #TWO_BLOCKS_SCORE = 1.0;
    static #THREE_BLOCKS_SCORE = 10.0;
    static #MINIMIZING_PLAYER_VICTORY_SCORE = -10E6;
    static #MAXIMIZING_PLAYER_VICTORY_SCORE = +10E6;
    
    evaluate(state, depth) {
        if (state.isWinningFor(X)) {
            return ConnectFourHeuristicFunction.#MINIMIZING_PLAYER_VICTORY_SCORE - depth;
        }
        
        if (state.isWinningFor(O)) {
            return ConnectFourHeuristicFunction.#MAXIMIZING_PLAYER_VICTORY_SCORE + depth;
        }
        
        return this.#evaluate2(state) +
               this.#evaluate3(state);
    }
    
    #evaluate2(state) {
        return this.#evaluate2Horizontal (state) +
               this.#evaluate2Vertical   (state) + 
               this.#evaluate2Ascending  (state) +
               this.#evaluate2Descending (state);
    }
    
    #evaluate3(state) {
        return this.#evaluate3Horizontal (state) +
               this.#evaluate3Vertical   (state) + 
               this.#evaluate3Ascending  (state) +
               this.#evaluate3Descending (state);
    }
    
    #evaluate2Horizontal(state) {
        let sum = 0.0;
        
        for (let y = 0; y < ROWS; y++) {
            for (let x = 0; x < COLUMNS - 1; x++) {
                if (state.get(x, y) === O &&
                    state.get(x + 1, y) === O) {
                    sum += ConnectFourHeuristicFunction.#TWO_BLOCKS_SCORE;
                } else if (state.get(x, y) === X &&
                           state.get(x + 1, y) === X) {
                    
                    sum -= ConnectFourHeuristicFunction.#TWO_BLOCKS_SCORE;
                }
            }
        }
        
        return sum;
    }
    
    #evaluate2Vertical(state) {
        let sum = 0.0;
        
        for (let y = 0; y < ROWS - 1; y++) {
            for (let x = 0; x < COLUMNS; x++) {
                if (state.get(x, y) === O &&
                    state.get(x, y + 1) === O) {
                    
                    sum += ConnectFourHeuristicFunction.#TWO_BLOCKS_SCORE;
                } else if (state.get(x, y) === X &&
                       state.get(x, y + 1) === X) {
                    
                    sum -= ConnectFourHeuristicFunction.#TWO_BLOCKS_SCORE;
                }
            }
        }
        
        return sum;
    }
    
    #evaluate2Ascending(state) {
        let sum = 0.0;
        
        for (let y = ROWS - 1; y > 0; y--) {
            for (let x = 0; x < COLUMNS - 1; x++) {
                if (state.get(x, y) === O &&
                    state.get(x + 1, y - 1) === O) {
                    sum += ConnectFourHeuristicFunction.#TWO_BLOCKS_SCORE;
                } else if (state.get(x, y) === X &&
                           state.get(x + 1, y - 1) === X) {
                    
                    sum -= ConnectFourHeuristicFunction.#TWO_BLOCKS_SCORE;
                }
            }
        }
        
        return sum;
    }
    
    #evaluate2Descending(state) {
        let sum = 0.0;
        
        for (let y = ROWS - 1; y > 0; y--) {
            for (let x = 1; x < COLUMNS; x++) {
                if (state.get(x, y) === O &&
                    state.get(x - 1, y - 1) === O) {
                    
                    sum += ConnectFourHeuristicFunction.#TWO_BLOCKS_SCORE;
                } else if (state.get(x, y) === X &&
                           state.get(x - 1, y - 1) === X) {
                    
                    sum -= ConnectFourHeuristicFunction.#TWO_BLOCKS_SCORE;
                }
            }
        }
        
        return sum;
    }
    
    #evaluate3Horizontal(state) {
        let sum = 0.0;
        
        for (let y = 0; y < ROWS; y++) {
            for (let x = 0; x < COLUMNS - 2; x++) {
                if (state.get(x, y) === O &&
                    state.get(x + 1, y) === O &&
                    state.get(x + 2, y) === O) {
                    
                    sum += ConnectFourHeuristicFunction.#THREE_BLOCKS_SCORE;
                } else if (state.get(x, y) === X &&
                        state.get(x + 1, y) === X &&
                        state.get(x + 2, y) === X) {
                    
                    sum -= ConnectFourHeuristicFunction.#THREE_BLOCKS_SCORE;
                }
            }
        }
        
        return sum;
    }
    
    #evaluate3Vertical(state) {
        let sum = 0.0;
        
        for (let y = 0; y < ROWS - 2; y++) {
            for (let x = 0; x < COLUMNS; x++) {
                if (state.get(x, y) === O &&
                    state.get(x, y + 1) === O &&
                    state.get(x, y + 2) === O) {
                    
                    sum += ConnectFourHeuristicFunction.#THREE_BLOCKS_SCORE;
                } else if (state.get(x, y) === X &&
                        state.get(x, y + 1) === X &&
                        state.get(x, y + 2) === X) {
                    
                    sum -= ConnectFourHeuristicFunction.#THREE_BLOCKS_SCORE;
                }
            }
        }
        
        return sum;
    }
    
    #evaluate3Ascending(state) {
        let sum = 0.0;
        
        for (let y = ROWS - 1; y > 1; y--) {
            for (let x = 0; x < COLUMNS - 2; x++) {
                if (state.get(x, y) === O &&
                    state.get(x + 1, y - 1) === O &&
                    state.get(x + 2, y - 2) === O) {
                    
                    sum += ConnectFourHeuristicFunction.#THREE_BLOCKS_SCORE;
                } else if (state.get(x, y) === X &&
                        state.get(x + 1, y - 1) === X && 
                        state.get(x + 2, y - 2) === X) {
                    
                    sum -= ConnectFourHeuristicFunction.#THREE_BLOCKS_SCORE;
                }
            }
        }
        
        return sum;
    }
    
    #evaluate3Descending(state) {
        let sum = 0.0;
        
        for (let y = ROWS - 1; y > 1; y--) {
            for (let x = 2; x < COLUMNS; x++) {
                if (state.get(x, y) === O &&
                    state.get(x - 1, y - 1) === O &&
                    state.get(x - 2, y - 2) === O) {
                    
                    sum += ConnectFourHeuristicFunction.#THREE_BLOCKS_SCORE;
                } else if (state.get(x, y) === X &&
                        state.get(x - 1, y - 1) === X &&
                        state.get(x - 2, y - 2) === X) {
                    
                    sum -= ConnectFourHeuristicFunction.#THREE_BLOCKS_SCORE;
                }
            }
        }
        
        return sum;
    }
}

class ConnectFourBoard {
    static ROWS = 6;
    static COLUMNS = 7;
    static VICTORY_LENGTH = 4;
    
    #boardData;
        
    #getCellChar(cellCharacter) {
        switch (cellCharacter) {
            case X:
                return "<span style='color: blue;'>X</span>";
                
            case O:
                return "<span style='color: red;'>O</span>";
                
            case EMPTY:
                return " ";
                
            default:
                throw "Should not get here.";
        }
    }
        
    #getWinningCellChar(cellCharacter) {
        switch (cellCharacter) {
            case X:
                return "<span style='color: white; background-color: black; font-weight: bold;'>X</span>";
                
            case O:
                return "<span style='color: white; background-color: black; font-weight: bold;'>O</span>";
                
            case EMPTY:
                return " ";
                
            default:
                throw "Should not get here.";
        }
    }
    
    constructor(other) {
        if (arguments.length === 1) {
            this.#boardData = [...other.#boardData];
        } else {
            this.#boardData = Array(ConnectFourBoard.ROWS *
                                    ConnectFourBoard.COLUMNS);
                            
            for (let i = 0, end = this.#boardData.length; i < end; i++) {
                this.#boardData[i] = EMPTY;
            }
        }
    }
    
    expand() {
        const children = new Array(ConnectFourBoard.COLUMNS);
        
        for (let x = 0; x < COLUMNS; x++) {
            if (makePly(x)) {
                const child = new ConnectFourBoard(this);
                children.push(child);
                unmakePly(x);
            }
        }
        
        return children;
    }
    
    get(x, y) {
        return this.#boardData[y * ConnectFourBoard.COLUMNS + x];
    }
    
    set(x, y, playerType) {
        this.#boardData[y * ConnectFourBoard.COLUMNS + x] = playerType;
    }
    
    toString() {
        let str = "";
        const winningPattern = this.getWinningPattern();
        
        function isInWinningPattern(winningPattern, x, y) {
            if (winningPattern === null) {
                return false;
            }
            
            for (const p of winningPattern) {
                if (p.x === x && p.y === y) {
                    return true;
                }
            }
            
            return false;
        }
        
        for (let y = 0; y < ConnectFourBoard.ROWS; y++) {
            for (let x = 0; x < ConnectFourBoard.COLUMNS; x++) {
                str += "|";
                
                const belongsToWinningPattern = 
                        isInWinningPattern(winningPattern, x, y);
                
                if (belongsToWinningPattern === false) {
                    str += this.#getCellChar(this.get(x, y));

                } else {
                    str += this.#getWinningCellChar(this.get(x, y));
                }
            }
            
            str += "|<br/>";
        }
        
        str += "+-+-+-+-+-+-+-+<br/>";
        str += " 1 2 3 4 5 6 7";
        
        return str;
    }
    
    isTie() {
        for (let x = 0; x < COLUMNS; x++) {
            if (this.get(x, 0) === EMPTY) {
                return false;
            }
        }
        
        return true;
    }
    
    makePly(x, playerType) {
        for (let y = ROWS - 1; y >= 0; y--) {
            if (this.get(x, y) === EMPTY) {
                this.set(x, y, playerType);
                return true;
            }
        }
        
        return false;
    }
    
    unmakePly(x) {
        for (let y = 0; y < ROWS; y++) {
            if (this.get(x, y) !== EMPTY) {
                this.set(x, y, EMPTY);
                return;
            }
        }
    }
    
    isTerminal() {
        return this.isWinningFor(O) ||
               this.isWinningFor(X) ||
               this.isTie();
    }
    
    getWinningPattern() {
        if (!this.isWinningFor(X) &&
            !this.isWinningFor(O)) {
            return null;
        }
        
        let winningPattern = null;
        
        for (let length = ROWS; 
                 length >= ConnectFourBoard.VICTORY_LENGTH; 
                 length--) {
        
            // Try load the vertical winning pattern:
            winningPattern = 
                    this.tryLoadVerticalWinningPattern(X, length);

            if (winningPattern !== null) {
                return winningPattern;
            }
        
            winningPattern = 
                    this.tryLoadVerticalWinningPattern(O, length);

            if (winningPattern !== null) {
                return winningPattern;
            }
            
            // Try to load the ascending winning pattern:
            winningPattern =
                this.tryLoadAscendingWinningPattern(X, length);
        
            if (winningPattern !== null) {
                return winningPattern;
            }
            
            winningPattern =
                this.tryLoadAscendingWinningPattern(O, length);
        
            if (winningPattern !== null) {
                return winningPattern;
            }

            // Try to load the descending winning pattern:
            winningPattern = 
                    this.tryLoadDescendingWinningPattern(X, length);

            if (winningPattern !== null) {
                return winningPattern;
            }

            winningPattern = 
                    this.tryLoadDescendingWinningPattern(O, length);
            
            if (winningPattern !== null) {
                return winningPattern;
            }
        }

        for (let length = COLUMNS; length >= ConnectFourBoard.VICTORY_LENGTH; length--) {
            
            winningPattern = 
                    this.tryLoadHorizontalWinningPattern(X, length);
            
            if (winningPattern !== null) {
                return winningPattern;
            }
            
            winningPattern = 
                    this.tryLoadHorizontalWinningPattern(O, length);
            
            if (winningPattern !== null) {
                return winningPattern;
            }
        }
        
        throw "Should not get here.";
    }
    
    hasHorizontalStreak(playerType, length) {
        const lastX = COLUMNS - length;
        
        for (let y = ROWS - 1; y >= 0; y--) {
            horizontalCheck:
            
            for (let x = 0; x <= lastX; x++) {
                
                for (let i = 0; i < length; i++) {
                    if (this.get(x + i, y) !== playerType) {
                        continue horizontalCheck;
                    }
                }
                
                return true;
            }
        }
        
        return false;
    }
    
    hasVerticalStreak(playerType, length) {
        const lastY = ROWS - length;
        
        for (let x = 0; x < COLUMNS; x++) {
            verticalCheck:
            
            for (let y = 0; y <= lastY; y++) {
                for (let i = 0; i < length; i++) {
                    if (this.get(x, y + i) !== playerType) {
                        continue verticalCheck;
                    }
                }
                
                return true;
            }
        }
        
        return false;
    }
    
    hasAscendingDiagonalStreak(playerType, length) {
        
        const lastX = COLUMNS - length;
        const lastY = length - 1;
        
        for (let y = ROWS - 1; y >= lastY; y--) {
            diagonalCheck:
            
            for (let x = 0; x <= lastX; x++) {
                for (let i = 0; i < length; i++) {
                    if (this.get(x + i, y - i) !== playerType) {
                        continue diagonalCheck;
                    }
                }
                
                return true;
            }
        }
        
        return false;
    }
    
    hasDescendingDiagonalStreak(playerType, length) {
        
        const firstX = length - 1;
        const lastY  = length - 1;
        
        for (let y = ROWS - 1; y >= lastY; y--) {
            diagonalCheck:
                    
            for (let x = firstX; x < COLUMNS; x++) {
                for (let i = 0; i < length; i++) {
                    if (this.get(x - i, y - i) !== playerType) {
                        continue diagonalCheck;
                    }
                }
                
                return true;
            }
        }
        
        return false;
    }
    
    isWinningFor(playerType) {
        return this.hasAscendingDiagonalStreak  (playerType, ConnectFourBoard.VICTORY_LENGTH) ||
               this.hasDescendingDiagonalStreak (playerType, ConnectFourBoard.VICTORY_LENGTH) ||
               this.hasHorizontalStreak         (playerType, ConnectFourBoard.VICTORY_LENGTH) ||
               this.hasVerticalStreak           (playerType, ConnectFourBoard.VICTORY_LENGTH);
    }
    
    tryLoadAscendingWinningPattern(playerType, length) {
        
        const lastX = COLUMNS - length;
        const lastY = length - 1;
        let winningPattern = [];
        
        for (let y = ROWS - 1; y >= lastY; y--) {
            diagonalCheck:
                    
            for (let x = 0; x <= lastX; x++) {
                for (let i = 0; i < length; i++) {
                    if (this.get(x + i, y - i) === playerType) {
                        winningPattern.push({x: x + i, y: y - i});
                        
                        if (winningPattern.length === length) {
                            return winningPattern;
                        }
                    } else {
                        winningPattern = [];
                        continue diagonalCheck;
                    }
                }
            }
        }
        
        return null;
    }
    
    tryLoadDescendingWinningPattern(playerType, length) {
        
        const firstX = length - 1;
        const lastY = ROWS - length;
        let winningPattern = [];
        
        for (let y = ROWS - 1; y > lastY; y--) {
            diagonalCheck:
                    
            for (let x = firstX; x < COLUMNS; x++) {
                for (let i = 0; i < length; i++) {
                    if (this.get(x - i, y - i) === playerType) {
                        winningPattern.push({x: x - i, y: y - i});
                        
                        if (winningPattern.length === length) {
                            return winningPattern;
                        }
                    } else {
                        winningPattern = [];
                        continue diagonalCheck;
                    }
                }
            }
        }
        
        return null;
    }
    
    tryLoadHorizontalWinningPattern(playerType, length) {
        
        const lastX = COLUMNS - length;
        let winningPattern = [];
        
        for (let y = ROWS - 1; y >= 0; y--) {
            horizontalCheck:
                    
            for (let x = 0; x <= lastX; x++) {
                for (let i = 0; i < length; i++) {
                    if (this.get(x + i, y) === playerType) {
                        winningPattern.push({x: x + i, y: y});
                        
                        if (winningPattern.length === length) {
                            return winningPattern;
                        }
                    } else {
                        winningPattern = [];
                        continue horizontalCheck;
                    }
                }
            }
        }
        
        return null;
    }
    
    tryLoadVerticalWinningPattern(playerType, length) {
        
        const lastY = ROWS - length;
        let winningPattern = [];
        
        for (let x = 0; x < COLUMNS; x++) {
            verticalCheck:
                    
            for (let y = 0; y <= lastY; y++) {
                for (let i = 0; i < length; i++) {
                    if (this.get(x, y + i) === playerType) {
                        winningPattern.push({x: x, y: y + i});
                
                        if (winningPattern.length === length) {
                            return winningPattern;
                        }
                    } else {
                        winningPattern = [];
                        continue verticalCheck;
                    }
                }
            }
        }
        
        return null;
    }
};

class ConnectFourAlphaBetaPruningSearchEngine {
    #bestMoveState;
    #heuristicFunction;
    
    constructor(heuristicFunction) {
        this.#heuristicFunction = heuristicFunction;
    }
    
    search(root, depth, playerType = O) {
        this.#bestMoveState = null;
        
        this.#alphaBetaPruningRootImpl(root,
                                       depth,
                                       playerType);
        
        return this.#bestMoveState;
    }
    
    #alphaBetaPruningRootImpl(root, depth, playerType) {
        
        if (playerType === O) {
            
            // Try to maximize the value:
            let alpha = Number.NEGATIVE_INFINITY;
            let value = Number.NEGATIVE_INFINITY;
            let tentativeValue = Number.NEGATIVE_INFINITY;
            
            for (let x = 0; x < ConnectFourBoard.COLUMNS; x++) {
                if (!root.makePly(x, O)) {
                    continue;
                }

                value = Math.max(value,
                                 this.#alphaBetaPruningImpl(
                                         root,
                                         depth - 1,
                                         alpha,
                                         Number.POSITIVE_INFINITY,
                                         X));
                
                if (tentativeValue < value) {
                    tentativeValue = value;
                    this.#bestMoveState = new ConnectFourBoard(root);
                }

                root.unmakePly(x);  
                
                alpha = Math.max(alpha, value);
            }
        } else {
            
            let beta  = Number.POSITIVE_INFINITY;
            let value = Number.POSITIVE_INFINITY;
            let tentativeValue = Number.POSITIVE_INFINITY;
            
            for (let x = 0; x < ConnectFourBoard.COLUMNS; x++) {
                if (!root.makePly(x, X)) {
                    continue;
                }

                value = Math.min(value,
                                 this.#alphaBetaPruningImpl(
                                         root,
                                         depth - 1,
                                         Number.NEGATIVE_INFINITY,
                                         beta,
                                         O));

                if (tentativeValue > value) {
                    tentativeValue = value;
                    this.#bestMoveState = new ConnectFourBoard(root);
                }

                root.unmakePly(x);
                
                beta = Math.min(beta, value);
            }
        }
    } 
    
    #alphaBetaPruningImpl(state, 
                          depth, 
                          alpha,
                          beta, 
                          playerType) {

        if (depth === 0 || state.isTerminal()) {
            return heuristicFunction.evaluate(state, depth);
        }
        
        if (playerType === O) {
            let value = Number.NEGATIVE_INFINITY;
            
            for (let x = 0; x < ConnectFourBoard.COLUMNS; x++) {
                if (!state.makePly(x, O)) {
                    continue;
                }
                
                value = Math.max(value, 
                                 this.#alphaBetaPruningImpl(state,
                                                            depth - 1,
                                                            alpha,
                                                            beta,
                                                            X));
                
                state.unmakePly(x);
                
                if (value > beta) {
                    break;
                }
                
                alpha = Math.max(alpha, value);
            }   
            
            return value;
        } else {
            let value = Number.POSITIVE_INFINITY;
            
            for (let x = 0; x < ConnectFourBoard.COLUMNS; x++) {
                if (!state.makePly(x, X)) {
                    continue;
                }
                
                value = Math.min(value,
                                 this.#alphaBetaPruningImpl(state,
                                                            depth - 1,
                                                            alpha,
                                                            beta,
                                                            O));
                
                state.unmakePly(x);
                
                if (value < alpha) {
                    break;
                }
                
                beta = Math.min(beta, value);
            }
            
            return value;
        }          
    }
}

class ConnectFourNegamaxSearchEngine {
    #heuristicFunction;
    
    constructor(heuristicFunction) {
        this.#heuristicFunction = heuristicFunction;
    }
    
    search(root, depth, playerType = O) {
        if (playerType === O) {
            return this.#negamaxRoot(root,
                                     depth,
                                     Number.NEGATIVE_INFINITY,
                                     Number.POSITIVE_INFINITY,
                                     +1);
        } else {
            return this.#negamaxRoot(root,
                                     depth,
                                     Number.NEGATIVE_INFINITY,
                                     Number.POSITIVE_INFINITY,
                                     -1);
        }
    }
    
    #negamaxRoot(root,
                 depth,
                 alpha,
                 beta,
                 color) {
        
        let value = Number.NEGATIVE_INFINITY;
        let bestMoveState = null;
        
        for (let x = 0; x < COLUMNS; x++) {
            if (!root.makePly(x, color === 1 ? O : X)) {
                continue;
            }
            
            const score = 
                    -this.#negamax(root,
                                   depth - 1,
                                   -beta,
                                   -alpha,
                                   -color);
            
            
            if (color === +1) {
                if (value < score) {
                    value = score;
                    bestMoveState = new ConnectFourBoard(root);
                }
            } else {
                if (value > score) {
                    value = score;
                    bestMoveState = new ConnectFourBoard(root);
                }
            }
            
            root.unmakePly(x);
            
            alpha = Math.max(alpha, value);
            
            if (alpha >= beta) {
                break;
            }
        }
        
        return bestMoveState;
    }
    
    #negamax(root, 
             depth,
             alpha,
             beta,
             color) {
        
        if (depth === 0 || root.isTerminal()) {
            return color * this.#heuristicFunction.evaluate(root, depth);
        }
        
        let value = Number.NEGATIVE_INFINITY;
        
        for (let x = 0; x < COLUMNS; x++) {
            if (!root.makePly(x, color === 1 ? O : X)) {
                
                continue;
            }
            
            value = Math.max(
                        value,
                        -this.#negamax(root, 
                                       depth - 1, 
                                       -beta, 
                                       -alpha, 
                                       -color));
            
            root.unmakePly(x);
            
            alpha = Math.max(alpha, value);
            
            if (alpha >= beta) {
                break;
            }
        }
        
        return value;
    }
}
#output-text-div {
    font-family: monospace;
    font-size: 15pt;
}
<!DOCTYPE html>
<html>
    <head>
        <title>AI vs. AI playing Connect 4</title>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <link rel='stylesheet' type="text/css" href='css/connect4.css' />
    </head>
    <body>
        <div id='output-text-div'></div>
        <script src='js/connect4ai.js'></script>
        <script>

            function flipTurn(playerTurn) {
                switch (playerTurn) {
                    case X:
                        return O;
                        
                    case O:
                        return X;
                        
                    default:
                        throw "Should not get here.";
                }
            }

            const output = document.getElementById('output-text-div');
            const heuristicFunction = new ConnectFourHeuristicFunction();
            const engineX = new ConnectFourAlphaBetaPruningSearchEngine(
                                    heuristicFunction);
                            
            const engineO = new ConnectFourNegamaxSearchEngine(
                                    heuristicFunction);
            const depth = 8;
            
            let turnNumber = 1;
            
            function getTurnNumberString() {
                if (turnNumber % 10 === 1) {
                    return turnNumber + "st";
                }
                
                if (turnNumber % 10 === 2) {
                    return turnNumber + "nd";
                }
                
                if (turnNumber % 10 === 3) {
                    return turnNumber + "rd";
                }
                
                return turnNumber + "th";
            }

            let engine = engineX;
            let state = new ConnectFourBoard();
            output.innerHTML += state.toString() + "<br/>";

            function millis() {
                return new Date().valueOf();
            }
            
            let totalDurationX = 0;
            let totalDurationO = 0;
            
            let turn = X;
            
            function getEngineName(engine) {
                if (engine === engineX) {
                    return "Alpha-beta pruning engine (X)";
                }
                
                if (engine === engineO) {
                    return "Negamax engine (O)";
                }
                
                throw "Should not get here.";
            }
            
            function gameLoop() {
                setTimeout(() => {
                    let ta = millis();
                    state = engine.search(state, depth, turn);
                    let tb = millis();
                    
                    if (turn === X) {
                        totalDurationX += tb - ta;
                    } else {
                        totalDurationO += tb - ta;
                    }
                    
                    output.innerHTML += state.toString();
                    output.innerHTML += "<br/>";
                    output.innerHTML += 
                            turn === X ? 
                                getEngineName(engine)
                                        + " made the "
                                        + getTurnNumberString()
                                        + " turn, duration: " 
                                        + (tb - ta) 
                                        + " milliseconds.<br/>"
                                :
                                getEngineName(engine) 
                                        + " made the "
                                        + getTurnNumberString()
                                        + " turn, duration: " 
                                        + (tb - ta) 
                                        + " milliseconds.<br/>";
                                
                    turnNumber++;
                                
                    if (state.isTie()) {
                        output.innerHTML += "RESULT: It's a tie.<br/>";
                        return;
                    }
                    
                    if (turn === X) {
                        if (state.isWinningFor(X)) {
                            output.innerHTML += "RESULT: X won.<br/>";
                            output.innerHTML += "X AI total duration: " + totalDurationX + " milliseconds.<br/>";
                            output.innerHTML += "O AI total duration: " + totalDurationO + " milliseconds.<br/>";
                            window.scrollTo(0, document.body.scrollHeight);
                            return;
                        }
                    } else {
                        if (state.isWinningFor(O)) {
                            output.innerHTML += "RESULT: O won.<br/>";
                            output.innerHTML += "X AI total duration: " + totalDurationX + " milliseconds.<br/>";
                            output.innerHTML += "O AI total duration: " + totalDurationO + " milliseconds.<br/>";
                            window.scrollTo(0, document.body.scrollHeight);
                            return;
                        }
                    }
                    
                    function flipEngine(engine) {
                        if (engine === engineX) {
                            return engineO;
                        }
                        
                        if (engine === engineO) {
                            return engineX;
                        }
                        
                        throw "Should not get here.";
                    }
                    
                    turn = flipTurn(turn);
                    engine = flipEngine(engine);
                    
                    if (!state.isTerminal()) {
                        // Run the next frame:
                        window.scrollTo(0, document.body.scrollHeight);
                        requestAnimationFrame(gameLoop);
                    } else {
                    }
                }, 0);
            }
            
            gameLoop();
        </script>
    </body>
</html>

Critique request

  1. First of all, I would like to receive general comments on my work.
  2. Also, I need your help to make my code snippets runnable from this very page so that people would not have to copy/save the files in order to run the actual battle.
\$\endgroup\$
1
  • \$\begingroup\$ This code has bugs. Eg the line if (makePly(x)) { in function ConnectFourBoard .expand() will throw an error, should be (my guess) if (this.makePly(x)) {. Please ensure that the code is working. \$\endgroup\$
    – Blindman67
    Commented Jun 17 at 20:16

1 Answer 1

1
\$\begingroup\$

D.R.Y. code (Don't Repeat Yourself)

Not a full review but it was painful to see all the repeated code so had to say something.

OMDG... The code is very repetitive!

In two objects there are groups of functions that can be rewritten to just one function in each object.

A little more for a lot less

The trick is to define the 4 directions

const dirs = {
    vert: {x: 0, y: 1},
    hori: {x: 1, y: 0},
    up:   {x: 1, y: -1},
    down: {x: 1, y: 1},
}

And use the direction information to help iterate the array of cells. (where to start, stop and step)

The resulting function will be only a few lines longer than just one of your functions. In total you could reduce the code size in half. (That a lot of code no longer needing to be maintained and understood)

Don't create what you already have

In the second object ConnectFourBoard you check for a winning streak (4 functions for 4 direction) and ONLY when a win is found do you call one of 4 functions to get the location of the win, repeating the same code you use to check for the win.

The check function should store the pattern, if the pattern wins you have it, if not the next check just overwrite the previous check.

Keep it DRY

I have pointed out only some of the repetitive code. Always keep the concept of DRY code in your head as you create code. It will save you endless hours writing and maintaining the same code written a dozen slightly different ways.

\$\endgroup\$
2
  • \$\begingroup\$ Nice one! Definitely will un-dry it when I have time. By the way, do you know where is the bug that fail running the code directly on this page? \$\endgroup\$
    – coderodde
    Commented Jun 19 at 17:02
  • \$\begingroup\$ @coderodde Re bugs? See comment on your question. That was one that popped out to me, not to say there are not more. \$\endgroup\$
    – Blindman67
    Commented Jun 20 at 6:40

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.