You can't brute-force chess, but you can brute-force its little cousin: tic-tac-toe. The same algorithm that powers chess engines — minimax — fits a 3×3 board in a few lines of code, and there it plays perfectly. Try to beat it. You can't: the best you can force is a draw.
<p class="hint">You are <b>X</b>. The machine is <b>O</b> and plays with <b>minimax</b> — it searches the whole game tree and always picks an optimal move. Go ahead, try to win. You can't: the best you can force is a draw.</p>
<div id="board" class="board"></div>
<p id="status" class="status">Your move — you are X.</p>
<div class="stats">
<span>Positions the machine examined last move: <b id="nodes">0</b></span>
</div>
<button id="reset" class="btn">Reset game</button>
* { box-sizing: border-box; }
.hint { font-size: 14px; color: #556; line-height: 1.5; }
.board {
display: grid; grid-template-columns: repeat(3, 72px);
grid-template-rows: repeat(3, 72px); gap: 6px; margin: 14px 0; justify-content: center;
}
.cell {
display: flex; align-items: center; justify-content: center;
font-size: 40px; font-weight: 700; background: #f3f5f8;
border: 2px solid #d7dee6; border-radius: 10px; cursor: pointer;
user-select: none; transition: background .15s;
}
.cell:hover { background: #e7ecf2; }
.cell.x { color: #1f6feb; }
.cell.o { color: #c0392b; }
.cell.taken { cursor: default; }
.status { text-align: center; font-weight: 600; color: #334; min-height: 22px; }
.stats { text-align: center; font-size: 13px; color: #667; margin-bottom: 10px; }
.btn {
display: block; margin: 0 auto; padding: 8px 18px; font-size: 14px;
border: none; border-radius: 8px; background: #1f6feb; color: #fff; cursor: pointer;
}
.btn:hover { background: #1a5fd0; }
// Tic-tac-toe with a perfect minimax opponent.
// 'X' = human, 'O' = machine. The machine maximizes its own outcome
// assuming the human plays optimally against it.
const HUMAN = 'X', AI = 'O';
let board = Array(9).fill('');
let over = false;
let nodes = 0;
const LINES = [
[0,1,2],[3,4,5],[6,7,8], // rows
[0,3,6],[1,4,7],[2,5,8], // cols
[0,4,8],[2,4,6] // diagonals
];
function winner(b) {
for (const [a,c,d] of LINES) {
if (b[a] && b[a] === b[c] && b[a] === b[d]) return b[a];
}
return b.includes('') ? null : 'draw';
}
// minimax: returns a score from the machine's point of view.
// +10 (minus depth) if AI wins, -10 (plus depth) if human wins, 0 for a draw.
// Subtracting depth makes it prefer faster wins and slower losses.
function minimax(b, isAI, depth) {
const w = winner(b);
if (w === AI) return 10 - depth;
if (w === HUMAN) return depth - 10;
if (w === 'draw') return 0;
let best = isAI ? -Infinity : Infinity;
for (let i = 0; i < 9; i++) {
if (b[i] !== '') continue;
nodes++; // count every position we explore
b[i] = isAI ? AI : HUMAN;
const score = minimax(b, !isAI, depth + 1);
b[i] = '';
best = isAI ? Math.max(best, score) : Math.min(best, score);
}
return best;
}
function bestMove() {
nodes = 0;
let move = -1, bestScore = -Infinity;
for (let i = 0; i < 9; i++) {
if (board[i] !== '') continue;
board[i] = AI;
const score = minimax(board, false, 0);
board[i] = '';
if (score > bestScore) { bestScore = score; move = i; }
}
return move;
}
const boardEl = document.getElementById('board');
const statusEl = document.getElementById('status');
const nodesEl = document.getElementById('nodes');
function render() {
boardEl.innerHTML = '';
board.forEach((v, i) => {
const c = document.createElement('div');
c.className = 'cell' + (v ? ' taken ' + v.toLowerCase() : '');
c.textContent = v;
if (!v && !over) c.onclick = () => play(i);
boardEl.appendChild(c);
});
}
function play(i) {
if (over || board[i]) return;
board[i] = HUMAN;
let w = winner(board);
if (!w) {
const m = bestMove();
if (m >= 0) board[m] = AI;
nodesEl.textContent = nodes;
w = winner(board);
}
render();
if (w === HUMAN) { statusEl.textContent = 'You win?! (a bug — report it!)'; over = true; }
else if (w === AI) { statusEl.textContent = 'The machine wins.'; over = true; }
else if (w === 'draw'){ statusEl.textContent = 'Draw — the best you can do.'; over = true; }
else statusEl.textContent = 'Your move — you are X.';
}
document.getElementById('reset').onclick = () => {
board = Array(9).fill(''); over = false; nodes = 0;
nodesEl.textContent = '0';
statusEl.textContent = 'Your move — you are X.';
render();
};
render();
<p class="hint">You are <b>X</b>. The machine is <b>O</b> and plays with <b>minimax</b> — it searches the whole game tree and always picks an optimal move. Go ahead, try to win. You can't: the best you can force is a draw.</p>
<div id="board" class="board"></div>
<p id="status" class="status">Your move — you are X.</p>
<div class="stats">
<span>Positions the machine examined last move: <b id="nodes">0</b></span>
</div>
<button id="reset" class="btn">Reset game</button>
* { box-sizing: border-box; }
.hint { font-size: 14px; color: #556; line-height: 1.5; }
.board {
display: grid; grid-template-columns: repeat(3, 72px);
grid-template-rows: repeat(3, 72px); gap: 6px; margin: 14px 0; justify-content: center;
}
.cell {
display: flex; align-items: center; justify-content: center;
font-size: 40px; font-weight: 700; background: #f3f5f8;
border: 2px solid #d7dee6; border-radius: 10px; cursor: pointer;
user-select: none; transition: background .15s;
}
.cell:hover { background: #e7ecf2; }
.cell.x { color: #1f6feb; }
.cell.o { color: #c0392b; }
.cell.taken { cursor: default; }
.status { text-align: center; font-weight: 600; color: #334; min-height: 22px; }
.stats { text-align: center; font-size: 13px; color: #667; margin-bottom: 10px; }
.btn {
display: block; margin: 0 auto; padding: 8px 18px; font-size: 14px;
border: none; border-radius: 8px; background: #1f6feb; color: #fff; cursor: pointer;
}
.btn:hover { background: #1a5fd0; }
// Tic-tac-toe with a perfect minimax opponent.
// 'X' = human, 'O' = machine. The machine maximizes its own outcome
// assuming the human plays optimally against it.
const HUMAN = 'X', AI = 'O';
let board = Array(9).fill('');
let over = false;
let nodes = 0;
const LINES = [
[0,1,2],[3,4,5],[6,7,8], // rows
[0,3,6],[1,4,7],[2,5,8], // cols
[0,4,8],[2,4,6] // diagonals
];
function winner(b) {
for (const [a,c,d] of LINES) {
if (b[a] && b[a] === b[c] && b[a] === b[d]) return b[a];
}
return b.includes('') ? null : 'draw';
}
// minimax: returns a score from the machine's point of view.
// +10 (minus depth) if AI wins, -10 (plus depth) if human wins, 0 for a draw.
// Subtracting depth makes it prefer faster wins and slower losses.
function minimax(b, isAI, depth) {
const w = winner(b);
if (w === AI) return 10 - depth;
if (w === HUMAN) return depth - 10;
if (w === 'draw') return 0;
let best = isAI ? -Infinity : Infinity;
for (let i = 0; i < 9; i++) {
if (b[i] !== '') continue;
nodes++; // count every position we explore
b[i] = isAI ? AI : HUMAN;
const score = minimax(b, !isAI, depth + 1);
b[i] = '';
best = isAI ? Math.max(best, score) : Math.min(best, score);
}
return best;
}
function bestMove() {
nodes = 0;
let move = -1, bestScore = -Infinity;
for (let i = 0; i < 9; i++) {
if (board[i] !== '') continue;
board[i] = AI;
const score = minimax(board, false, 0);
board[i] = '';
if (score > bestScore) { bestScore = score; move = i; }
}
return move;
}
const boardEl = document.getElementById('board');
const statusEl = document.getElementById('status');
const nodesEl = document.getElementById('nodes');
function render() {
boardEl.innerHTML = '';
board.forEach((v, i) => {
const c = document.createElement('div');
c.className = 'cell' + (v ? ' taken ' + v.toLowerCase() : '');
c.textContent = v;
if (!v && !over) c.onclick = () => play(i);
boardEl.appendChild(c);
});
}
function play(i) {
if (over || board[i]) return;
board[i] = HUMAN;
let w = winner(board);
if (!w) {
const m = bestMove();
if (m >= 0) board[m] = AI;
nodesEl.textContent = nodes;
w = winner(board);
}
render();
if (w === HUMAN) { statusEl.textContent = 'You win?! (a bug — report it!)'; over = true; }
else if (w === AI) { statusEl.textContent = 'The machine wins.'; over = true; }
else if (w === 'draw'){ statusEl.textContent = 'Draw — the best you can do.'; over = true; }
else statusEl.textContent = 'Your move — you are X.';
}
document.getElementById('reset').onclick = () => {
board = Array(9).fill(''); over = false; nodes = 0;
nodesEl.textContent = '0';
statusEl.textContent = 'Your move — you are X.';
render();
};
render();
Minimax explores the game tree assuming both players play optimally — the machine maximizes its outcome while assuming you will minimize it. Edit the code, watch how it picks each move, and notice the counter: even this trivial game has hundreds of positions. Now multiply that by the chess explosion.
Comments
Loading comments...