/* * 打分法 AI(贪心策略) * 对每个候选空位计算「己方落此处的进攻分 + 0.9 × 对方落此处的防守分」, * 取总分最高的一点。分值表的相对大小自然形成优先级: * 己方成五 > 挡对方成五 > 己方活四 > 挡对方活四 > … * 浏览器中挂到 window.GomokuAI,Node 中通过 module.exports 导出供测试使用。 */ (function (root, factory) { const api = factory(); if (typeof module !== 'undefined' && module.exports) { module.exports = api; } else { root.GomokuAI = api; } })(typeof window !== 'undefined' ? window : globalThis, function () { const SIZE = 15; const EMPTY = 0; const BLACK = 1; const WHITE = 2; // 横、竖、两条斜线 const DIRECTIONS = [[1, 0], [0, 1], [1, 1], [1, -1]]; const DEFENSE_WEIGHT = 0.9; // 防守分权重:略低于进攻,能赢时优先赢 const CENTER = { x: 7, y: 7 }; // 棋形分值表:按优先级从高到低排列,每个方向命中一个即止 // 模式串中 '1' = 己方,'0' = 空位,'2' = 对方棋子或边界 const PATTERN_TABLE = [ [['11111'], 1000000], // 连五 [['011110'], 100000], // 活四 [['011112', '211110', '10111', '11011', '11101'], 10000], // 冲四(含跳四) [['011100', '001110', '010110', '011010'], 8000], // 活三(含跳活三) [['11100', '00111', '11010', '01011', '10110', '01101', '10011', '11001', '10101'], 500], // 眠三 [['001100', '01010'], 400], // 活二 [['000112', '211000', '001012', '210100', '010012', '210010', '10001'], 50], // 眠二 ]; // 以 (x, y) 为中心、沿 (dx, dy) 方向取 9 格窗口,假设己方落在中心后转成模式串 function buildWindow(board, x, y, dx, dy, color) { let s = ''; for (let i = -4; i <= 4; i++) { if (i === 0) { s += '1'; continue; } const nx = x + dx * i; const ny = y + dy * i; if (nx < 0 || nx >= SIZE || ny < 0 || ny >= SIZE) { s += '2'; continue; } const c = board[ny][nx]; s += c === EMPTY ? '0' : c === color ? '1' : '2'; } return s; } // 评估 color 方落在 (x, y) 的价值:4 个方向的棋形分之和 function scorePoint(board, x, y, color) { let total = 0; for (const [dx, dy] of DIRECTIONS) { const win = buildWindow(board, x, y, dx, dy, color); for (const [patterns, score] of PATTERN_TABLE) { if (patterns.some(p => win.includes(p))) { total += score; break; } } } return total; } function hasNeighbor(board, x, y, range) { for (let dy = -range; dy <= range; dy++) { for (let dx = -range; dx <= range; dx++) { if (dx === 0 && dy === 0) continue; const nx = x + dx; const ny = y + dy; if (nx >= 0 && nx < SIZE && ny >= 0 && ny < SIZE && board[ny][nx] !== EMPTY) return true; } } return false; } // 候选点:已有棋子周围 2 格内的空位;空盘直接落天元 function findCandidates(board) { const result = []; let hasStone = false; for (let y = 0; y < SIZE; y++) { for (let x = 0; x < SIZE; x++) { if (board[y][x] !== EMPTY) { hasStone = true; break; } } if (hasStone) break; } if (!hasStone) return [CENTER]; for (let y = 0; y < SIZE; y++) { for (let x = 0; x < SIZE; x++) { if (board[y][x] === EMPTY && hasNeighbor(board, x, y, 2)) { result.push({ x, y }); } } } return result; } function nextMove(game, color) { const me = color || game.currentPlayer; const opp = me === BLACK ? WHITE : BLACK; let best = []; let bestScore = -Infinity; for (const spot of findCandidates(game.board)) { const score = scorePoint(game.board, spot.x, spot.y, me) + DEFENSE_WEIGHT * scorePoint(game.board, spot.x, spot.y, opp); if (score > bestScore) { bestScore = score; best = [spot]; } else if (score === bestScore) { best.push(spot); } } return best[Math.floor(Math.random() * best.length)]; } return { nextMove, scorePoint, findCandidates }; });