export function scorePattern(count, openEnds) { if (count >= 5) return 1_000_000; // win if (count === 4 && openEnds === 2) return 50_000; // open four - forced win if (count === 4 && openEnds === 1) return 1_000; // closed four if (count === 3 && openEnds === 2) return 1_000; // open three - strong if (count === 3 && openEnds === 1) return 100; if (count === 2 && openEnds === 2) return 100; if (count === 2 && openEnds === 1) return 10; return 0; } CODE_BLOCK: export function scorePattern(count, openEnds) { if (count >= 5) return 1_000_000; // win if (count === 4 && openEnds === 2) return 50_000; // open four - forced win if (count === 4 && openEnds === 1) return 1_000; // closed four if (count === 3 && openEnds === 2) return 1_000; // open three - strong if (count === 3 && openEnds === 1) return 100; if (count === 2 && openEnds === 2) return 100; if (count === 2 && openEnds === 1) return 10; return 0; } CODE_BLOCK: export function scorePattern(count, openEnds) { if (count >= 5) return 1_000_000; // win if (count === 4 && openEnds === 2) return 50_000; // open four - forced win if (count === 4 && openEnds === 1) return 1_000; // closed four if (count === 3 && openEnds === 2) return 1_000; // open three - strong if (count === 3 && openEnds === 1) return 100; if (count === 2 && openEnds === 2) return 100; if (count === 2 && openEnds === 1) return 10; return 0; } CODE_BLOCK: function minimax(board, depth, isMax, player, alpha, beta) { if (depth === 0 || isTerminal(board)) return evaluateBoard(board, player); const moves = getNearbyCells(board, 2); if (isMax) { let best = -Infinity; for (const [r, c] of moves) { const newBoard = placeStone(board, r, c, player); best = Math.max(best, minimax(newBoard, depth - 1, false, player, alpha, beta)); alpha = Math.max(alpha, best); if (beta <= alpha) break; // α-β cutoff } return best; } else { // symmetric min branch } } CODE_BLOCK: function minimax(board, depth, isMax, player, alpha, beta) { if (depth === 0 || isTerminal(board)) return evaluateBoard(board, player); const moves = getNearbyCells(board, 2); if (isMax) { let best = -Infinity; for (const [r, c] of moves) { const newBoard = placeStone(board, r, c, player); best = Math.max(best, minimax(newBoard, depth - 1, false, player, alpha, beta)); alpha = Math.max(alpha, best); if (beta <= alpha) break; // α-β cutoff } return best; } else { // symmetric min branch } } CODE_BLOCK: function minimax(board, depth, isMax, player, alpha, beta) { if (depth === 0 || isTerminal(board)) return evaluateBoard(board, player); const moves = getNearbyCells(board, 2); if (isMax) { let best = -Infinity; for (const [r, c] of moves) { const newBoard = placeStone(board, r, c, player); best = Math.max(best, minimax(newBoard, depth - 1, false, player, alpha, beta)); alpha = Math.max(alpha, best); if (beta <= alpha) break; // α-β cutoff } return best; } else { // symmetric min branch } } COMMAND_BLOCK: export function getNearbyCells(board, radius = 2) { const nearby = new Set(); for (let r = 0; r < BOARD_SIZE; r++) { for (let c = 0; c < BOARD_SIZE; c++) { if (board[r][c] !== EMPTY) continue; // Check if any stone exists within `radius` for (let dr = -radius; dr <= radius; dr++) { for (let dc = -radius; dc <= radius; dc++) { const nr = r + dr, nc = c + dc; if (nr >= 0 && nr < BOARD_SIZE && nc >= 0 && nc < BOARD_SIZE && board[nr][nc] !== EMPTY) { nearby.add(`${r},${c}`); } } } } } return [...nearby].map(s => s.split(',').map(Number)); } COMMAND_BLOCK: export function getNearbyCells(board, radius = 2) { const nearby = new Set(); for (let r = 0; r < BOARD_SIZE; r++) { for (let c = 0; c < BOARD_SIZE; c++) { if (board[r][c] !== EMPTY) continue; // Check if any stone exists within `radius` for (let dr = -radius; dr <= radius; dr++) { for (let dc = -radius; dc <= radius; dc++) { const nr = r + dr, nc = c + dc; if (nr >= 0 && nr < BOARD_SIZE && nc >= 0 && nc < BOARD_SIZE && board[nr][nc] !== EMPTY) { nearby.add(`${r},${c}`); } } } } } return [...nearby].map(s => s.split(',').map(Number)); } COMMAND_BLOCK: export function getNearbyCells(board, radius = 2) { const nearby = new Set(); for (let r = 0; r < BOARD_SIZE; r++) { for (let c = 0; c < BOARD_SIZE; c++) { if (board[r][c] !== EMPTY) continue; // Check if any stone exists within `radius` for (let dr = -radius; dr <= radius; dr++) { for (let dc = -radius; dc <= radius; dc++) { const nr = r + dr, nc = c + dc; if (nr >= 0 && nr < BOARD_SIZE && nc >= 0 && nc < BOARD_SIZE && board[nr][nc] !== EMPTY) { nearby.add(`${r},${c}`); } } } } } return [...nearby].map(s => s.split(',').map(Number)); } CODE_BLOCK: export function checkWin(board, row, col, player) { const directions = [[0,1], [1,0], [1,1], [1,-1]]; for (const [dr, dc] of directions) { let count = 1; // Walk positive direction let r = row + dr, c = col + dc; while (inBounds(r, c) && board[r][c] === player) { count++; r += dr; c += dc; } // Walk negative direction r = row - dr; c = col - dc; while (inBounds(r, c) && board[r][c] === player) { count++; r -= dr; c -= dc; } if (count >= 5) return true; } return false; } CODE_BLOCK: export function checkWin(board, row, col, player) { const directions = [[0,1], [1,0], [1,1], [1,-1]]; for (const [dr, dc] of directions) { let count = 1; // Walk positive direction let r = row + dr, c = col + dc; while (inBounds(r, c) && board[r][c] === player) { count++; r += dr; c += dc; } // Walk negative direction r = row - dr; c = col - dc; while (inBounds(r, c) && board[r][c] === player) { count++; r -= dr; c -= dc; } if (count >= 5) return true; } return false; } CODE_BLOCK: export function checkWin(board, row, col, player) { const directions = [[0,1], [1,0], [1,1], [1,-1]]; for (const [dr, dc] of directions) { let count = 1; // Walk positive direction let r = row + dr, c = col + dc; while (inBounds(r, c) && board[r][c] === player) { count++; r += dr; c += dc; } // Walk negative direction r = row - dr; c = col - dc; while (inBounds(r, c) && board[r][c] === player) { count++; r -= dr; c -= dc; } if (count >= 5) return true; } return false; } - 15×15 standard Gomoku board (Canvas rendering)
- Minimax + alpha-beta pruning - 3 difficulty levels (depth 1 / 2 / 4)
- Pattern-based evaluator (open/closed, 2-4 in a row)
- Move restriction to radius-2 of existing stones
- Immediate-threat blocking
- Undo, move history, last-move highlight
- Japanese / English UI
- Zero dependencies, 42 tests - 📦 Repo: https://github.com/sen-ltd/gomoku-ai - 🌐 Live: https://sen.ltd/portfolio/gomoku-ai/ - 🏢 Company: https://sen.ltd/