game/js/ai.js

124 lines
4.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* 打分法 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 };
});