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.