2
\$\begingroup\$

This time, I have a Javascript program that runs a Connect Four match between two Alpha-beta pruning based AI bots. (See this page.)

ai-battle.html:

<!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 engine = new ConnectFourSearchEngine(heuristicFunction);
            const depth = 8;

            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 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 ? 
                                        "X AI duration: " + (tb - ta) + " milliseconds.<br/>" :
                                        "O AI duration: " + (tb - ta) + " milliseconds.<br/>";
                                
                    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;
                        }
                    }
                    
                    turn = flipTurn(turn);
                    
                    if (!state.isTerminal()) {
                        // Run the next frame:
                        window.scrollTo(0, document.body.scrollHeight);
                        requestAnimationFrame(gameLoop);
                    } else {
                    }
                }, 0);
            }
            
            gameLoop();
        </script>
    </body>
</html>

js/connect4ai.js:

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 ConnectFourBoard {
    static ROWS = 6;
    static COLUMNS = 7;
    static VICTORY_LENGTH = 4;
    
    #boardData;
        
    #getCellChar(cellCharacter) {
        switch (cellCharacter) {
            case X:
                return "X";
                
            case O:
                return "O";
                
            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;
            }
        }
    }
    
    get(x, y) {
        return this.#boardData[y * ConnectFourBoard.COLUMNS + x];
    }
    
    set(x, y, playerType) {
        this.#boardData[y * ConnectFourBoard.COLUMNS + x] = playerType;
    }
    
    toString() {
        let str = "";
        
        for (let y = 0; y < ConnectFourBoard.ROWS; y++) {
            for (let x = 0; x < ConnectFourBoard.COLUMNS; x++) {
                str += "|";
                str += this.#getCellChar(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 = 
                    tryLoadVerticalWinningPattern(X, length);

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

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

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

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

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

        for (let length = COLUMNS; length >= ConnectFourBoard.VICTORY_LENGTH; length--) {
            
            winningPattern = 
                    tryLoadHorizontalWinningPattern(X, length);
            
            if (winningPattern !== null) {
                return winningPattern;
            }
            
            winningPattern = 
                    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;
        const 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;
        const 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;
        const 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;
        const 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 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 ConnectFourSearchEngine {
    #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;
        }          
    }
}

Typical output

| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | | | | | |
+-+-+-+-+-+-+-+
1 2 3 4 5 6 7
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | |X| | | |
+-+-+-+-+-+-+-+
1 2 3 4 5 6 7
X AI duration: 763 milliseconds.
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | | | | | |
| |O| |X| | | |
+-+-+-+-+-+-+-+
1 2 3 4 5 6 7
O AI duration: 1249 milliseconds.
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | | | | | |
| |O| |X|X| | |
+-+-+-+-+-+-+-+
1 2 3 4 5 6 7
X AI duration: 718 milliseconds.
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | | | | | |
| |O| |X|X|O| |
+-+-+-+-+-+-+-+
1 2 3 4 5 6 7
O AI duration: 2258 milliseconds.
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | |X| | | |
| |O| |X|X|O| |
+-+-+-+-+-+-+-+
1 2 3 4 5 6 7
X AI duration: 937 milliseconds.
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | |O| | | |
| | | |X| | | |
| |O| |X|X|O| |
+-+-+-+-+-+-+-+
1 2 3 4 5 6 7
O AI duration: 1006 milliseconds.
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | |O| | | |
| | | |X|X| | |
| |O| |X|X|O| |
+-+-+-+-+-+-+-+
1 2 3 4 5 6 7
X AI duration: 948 milliseconds.
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | |O|O| | |
| | | |X|X| | |
| |O| |X|X|O| |
+-+-+-+-+-+-+-+
1 2 3 4 5 6 7
O AI duration: 1390 milliseconds.
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | |O|O| | |
| | | |X|X|X| |
| |O| |X|X|O| |
+-+-+-+-+-+-+-+
1 2 3 4 5 6 7
X AI duration: 1604 milliseconds.
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | |O|O|O| |
| | | |X|X|X| |
| |O| |X|X|O| |
+-+-+-+-+-+-+-+
1 2 3 4 5 6 7
O AI duration: 1521 milliseconds.
| | | | | | | |
| | | | | | | |
| | | | |X| | |
| | | |O|O|O| |
| | | |X|X|X| |
| |O| |X|X|O| |
+-+-+-+-+-+-+-+
1 2 3 4 5 6 7
X AI duration: 1058 milliseconds.
| | | | | | | |
| | | | | | | |
| | | |O|X| | |
| | | |O|O|O| |
| | | |X|X|X| |
| |O| |X|X|O| |
+-+-+-+-+-+-+-+
1 2 3 4 5 6 7
O AI duration: 916 milliseconds.
| | | | | | | |
| | | |X| | | |
| | | |O|X| | |
| | | |O|O|O| |
| | | |X|X|X| |
| |O| |X|X|O| |
+-+-+-+-+-+-+-+
1 2 3 4 5 6 7
X AI duration: 684 milliseconds.
| | | | | | | |
| | | |X| | | |
| | | |O|X|O| |
| | | |O|O|O| |
| | | |X|X|X| |
| |O| |X|X|O| |
+-+-+-+-+-+-+-+
1 2 3 4 5 6 7
O AI duration: 749 milliseconds.
| | | | | | | |
| | | |X| |X| |
| | | |O|X|O| |
| | | |O|O|O| |
| | | |X|X|X| |
| |O| |X|X|O| |
+-+-+-+-+-+-+-+
1 2 3 4 5 6 7
X AI duration: 827 milliseconds.
| | | | | | | |
| | | |X|O|X| |
| | | |O|X|O| |
| | | |O|O|O| |
| | | |X|X|X| |
| |O| |X|X|O| |
+-+-+-+-+-+-+-+
1 2 3 4 5 6 7
O AI duration: 495 milliseconds.
| | | | | | | |
| | | |X|O|X| |
| | | |O|X|O| |
| | | |O|O|O| |
| |X| |X|X|X| |
| |O| |X|X|O| |
+-+-+-+-+-+-+-+
1 2 3 4 5 6 7
X AI duration: 355 milliseconds.
| | | |O| | | |
| | | |X|O|X| |
| | | |O|X|O| |
| | | |O|O|O| |
| |X| |X|X|X| |
| |O| |X|X|O| |
+-+-+-+-+-+-+-+
1 2 3 4 5 6 7
O AI duration: 252 milliseconds.
| | | |O| | | |
| | | |X|O|X| |
| | | |O|X|O| |
| | | |O|O|O| |
| |X| |X|X|X| |
|X|O| |X|X|O| |
+-+-+-+-+-+-+-+
1 2 3 4 5 6 7
X AI duration: 121 milliseconds.
| | | |O| |O| |
| | | |X|O|X| |
| | | |O|X|O| |
| | | |O|O|O| |
| |X| |X|X|X| |
|X|O| |X|X|O| |
+-+-+-+-+-+-+-+
1 2 3 4 5 6 7
O AI duration: 162 milliseconds.
| | | |O|X|O| |
| | | |X|O|X| |
| | | |O|X|O| |
| | | |O|O|O| |
| |X| |X|X|X| |
|X|O| |X|X|O| |
+-+-+-+-+-+-+-+
1 2 3 4 5 6 7
X AI duration: 70 milliseconds.
| | | |O|X|O| |
| | | |X|O|X| |
| | | |O|X|O| |
| | | |O|O|O| |
|O|X| |X|X|X| |
|X|O| |X|X|O| |
+-+-+-+-+-+-+-+
1 2 3 4 5 6 7
O AI duration: 31 milliseconds.
| | | |O|X|O| |
| | | |X|O|X| |
| | | |O|X|O| |
| | | |O|O|O| |
|O|X| |X|X|X| |
|X|O|X|X|X|O| |
+-+-+-+-+-+-+-+
1 2 3 4 5 6 7
X AI duration: 29 milliseconds.
| | | |O|X|O| |
| | | |X|O|X| |
| | | |O|X|O| |
| | | |O|O|O| |
|O|X|O|X|X|X| |
|X|O|X|X|X|O| |
+-+-+-+-+-+-+-+
1 2 3 4 5 6 7
O AI duration: 22 milliseconds.
| | | |O|X|O| |
| | | |X|O|X| |
| | | |O|X|O| |
| | |X|O|O|O| |
|O|X|O|X|X|X| |
|X|O|X|X|X|O| |
+-+-+-+-+-+-+-+
1 2 3 4 5 6 7
X AI duration: 19 milliseconds.
| | | |O|X|O| |
| | | |X|O|X| |
| | |O|O|X|O| |
| | |X|O|O|O| |
|O|X|O|X|X|X| |
|X|O|X|X|X|O| |
+-+-+-+-+-+-+-+
1 2 3 4 5 6 7
O AI duration: 15 milliseconds.
| | | |O|X|O| |
| | | |X|O|X| |
| | |O|O|X|O| |
|X| |X|O|O|O| |
|O|X|O|X|X|X| |
|X|O|X|X|X|O| |
+-+-+-+-+-+-+-+
1 2 3 4 5 6 7
X AI duration: 12 milliseconds.
| | | |O|X|O| |
| | | |X|O|X| |
|O| |O|O|X|O| |
|X| |X|O|O|O| |
|O|X|O|X|X|X| |
|X|O|X|X|X|O| |
+-+-+-+-+-+-+-+
1 2 3 4 5 6 7
O AI duration: 7 milliseconds.
| | | |O|X|O| |
| | |X|X|O|X| |
|O| |O|O|X|O| |
|X| |X|O|O|O| |
|O|X|O|X|X|X| |
|X|O|X|X|X|O| |
+-+-+-+-+-+-+-+
1 2 3 4 5 6 7
X AI duration: 3 milliseconds.
| | |O|O|X|O| |
| | |X|X|O|X| |
|O| |O|O|X|O| |
|X| |X|O|O|O| |
|O|X|O|X|X|X| |
|X|O|X|X|X|O| |
+-+-+-+-+-+-+-+
1 2 3 4 5 6 7
O AI duration: 2 milliseconds.
| | |O|O|X|O| |
|X| |X|X|O|X| |
|O| |O|O|X|O| |
|X| |X|O|O|O| |
|O|X|O|X|X|X| |
|X|O|X|X|X|O| |
+-+-+-+-+-+-+-+
1 2 3 4 5 6 7
X AI duration: 1 milliseconds.
|O| |O|O|X|O| |
|X| |X|X|O|X| |
|O| |O|O|X|O| |
|X| |X|O|O|O| |
|O|X|O|X|X|X| |
|X|O|X|X|X|O| |
+-+-+-+-+-+-+-+
1 2 3 4 5 6 7
O AI duration: 0 milliseconds.
|O| |O|O|X|O| |
|X| |X|X|O|X| |
|O| |O|O|X|O| |
|X| |X|O|O|O| |
|O|X|O|X|X|X| |
|X|O|X|X|X|O|X|
+-+-+-+-+-+-+-+
1 2 3 4 5 6 7
X AI duration: 0 milliseconds.
|O| |O|O|X|O| |
|X| |X|X|O|X| |
|O| |O|O|X|O| |
|X| |X|O|O|O| |
|O|X|O|X|X|X|O|
|X|O|X|X|X|O|X|
+-+-+-+-+-+-+-+
1 2 3 4 5 6 7
O AI duration: 0 milliseconds.
|O| |O|O|X|O| |
|X| |X|X|O|X| |
|O| |O|O|X|O| |
|X| |X|O|O|O|X|
|O|X|O|X|X|X|O|
|X|O|X|X|X|O|X|
+-+-+-+-+-+-+-+
1 2 3 4 5 6 7
X AI duration: 0 milliseconds.
|O| |O|O|X|O| |
|X| |X|X|O|X| |
|O| |O|O|X|O|O|
|X| |X|O|O|O|X|
|O|X|O|X|X|X|O|
|X|O|X|X|X|O|X|
+-+-+-+-+-+-+-+
1 2 3 4 5 6 7
O AI duration: 0 milliseconds.
|O| |O|O|X|O| |
|X| |X|X|O|X|X|
|O| |O|O|X|O|O|
|X| |X|O|O|O|X|
|O|X|O|X|X|X|O|
|X|O|X|X|X|O|X|
+-+-+-+-+-+-+-+
1 2 3 4 5 6 7
X AI duration: 0 milliseconds.
|O| |O|O|X|O|O|
|X| |X|X|O|X|X|
|O| |O|O|X|O|O|
|X| |X|O|O|O|X|
|O|X|O|X|X|X|O|
|X|O|X|X|X|O|X|
+-+-+-+-+-+-+-+
1 2 3 4 5 6 7
O AI duration: 0 milliseconds.
|O| |O|O|X|O|O|
|X| |X|X|O|X|X|
|O| |O|O|X|O|O|
|X|X|X|O|O|O|X|
|O|X|O|X|X|X|O|
|X|O|X|X|X|O|X|
+-+-+-+-+-+-+-+
1 2 3 4 5 6 7
X AI duration: 0 milliseconds.
|O| |O|O|X|O|O|
|X| |X|X|O|X|X|
|O|O|O|O|X|O|O|
|X|X|X|O|O|O|X|
|O|X|O|X|X|X|O|
|X|O|X|X|X|O|X|
+-+-+-+-+-+-+-+
1 2 3 4 5 6 7
O AI duration: 0 milliseconds.
RESULT: O won.
X AI total duration: 8149 milliseconds.
O AI total duration: 10075 milliseconds.

Critique request

As always, I would like to receive any commentary on my work.

\$\endgroup\$

0

Your Answer

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