124 lines
4.3 KiB
JavaScript
124 lines
4.3 KiB
JavaScript
/*
|
||
* 打分法 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 };
|
||
});
|