我正在用Javascript开发一个简单的五连珠(五子棋)AI,纯粹是为了好玩,使用了Minimax算法和Alpha-Beta剪枝。我只是按照网上的教程进行操作,但不知为何,我无法让它正常工作。我认为代码中可能存在逻辑错误,因为在下面的代码中,如果AI在第二行第三列放置棋子,会形成一个分叉,但它并未被识别为bestMove
。
奇怪的是,如果我设置一个断点,那个位置(row: 1, col: 2
)经常被识别为winningMove
,但不知为何,Minimax函数从未将bestMove
计算为那个位置,尽管我认为它显然应该是那个位置。也就是说,如果AI在那里放置棋子,下一回合它几乎可以立即获胜,因为这会导致多个方向的胜利。
也就是说,如果AI在白色的2号位置放置棋子,那么在下一个AI回合中,可以实现垂直胜利或水平胜利,因为人类玩家只能阻止其中一个方向的胜利:
const ROWS = 9;const COLS = 9;const LEN = 5;const EMPTY = 0;const HUMAN = 1;const COMP = 2;function checkDirection(grid, who, currChain, sRow, sCol, incRow, incCol) { let newChain = 0; while (currChain + newChain < LEN) { const row = sRow + (incRow * (newChain + 1)); const col = sCol + (incCol * (newChain + 1)); if (grid[row * COLS + col] !== who) { break; } newChain++; } return newChain;}function lineCheck(grid, who, sRow, sCol, mRow, mCol) { let chain = 1; chain += checkDirection(grid, who, 0, sRow, sCol, mRow, mCol); chain += checkDirection(grid, who, chain, sRow, sCol, -mRow, -mCol); return chain >= LEN;}function isWinningMove(grid, who, row, col) { return lineCheck(grid, who, row, col, 1, 0) || lineCheck(grid, who, row, col, 0, 1) || lineCheck(grid, who, row, col, 1, 1) || lineCheck(grid, who, row, col, -1, 1);}function getTile(grid, row, col) { if (row < 0 || col < 0 || row >= ROWS || col >= COLS) { return -1; } return grid[row * COLS + col];}function hasNeighbor(board, row, col) { if (getTile(board, row - 1, col - 1) > 0) { return true; } if (getTile(board, row - 1, col + 1) > 0) { return true; } if (getTile(board, row + 1, col - 1) > 0) { return true; } if (getTile(board, row + 1, col + 1) > 0) { return true; } if (getTile(board, row - 1, col) > 0) { return true; } if (getTile(board, row + 1, col) > 0) { return true; } if (getTile(board, row, col - 1) > 0) { return true; } if (getTile(board, row, col + 1) > 0) { return true; } return false;}let bestMove = Number.MIN_SAFE_INTEGER;function minimax(board, depth, alpha, beta, player, latestRow, latestCol) { if (depth === 0) { return evaluatePlayerBoard(board, player, player === COMP ? HUMAN : COMP, latestRow, latestCol); } if (isWinningMove(board, player, latestRow, latestCol)) { return 1000000; } if (player === COMP) { let maxEval = Number.MIN_SAFE_INTEGER; for (let row = 0; row < ROWS; row++) { for (let col = 0; col < COLS; col++) { const idx = row * COLS + col; const tileValue = board[idx]; if (tileValue > 0 || !hasNeighbor(board, row, col)) { continue; } board[idx] = player; const evaluation = minimax(board, depth - 1, alpha, beta, HUMAN, row, col); board[idx] = tileValue; if (evaluation > maxEval) { maxEval = evaluation; bestMove = idx; } alpha = Math.max(alpha, evaluation); if (beta <= alpha) { return maxEval; } } } return maxEval; } else { let minEval = Number.MAX_SAFE_INTEGER; for (let row = 0; row < ROWS; row++) { for (let col = 0; col < COLS; col++) { const idx = row * COLS + col; const tileValue = board[idx]; if (tileValue > 0 || !hasNeighbor(board, row, col)) { continue; } board[idx] = player; const evaluation = minimax(board, depth - 1, alpha, beta, COMP, row, col); board[idx] = tileValue; if (evaluation < minEval) { minEval = evaluation; } beta = Math.min(beta, evaluation); if (beta <= alpha) { return minEval; } } } return minEval; }}function evaluatePlayerBoard(grid, who, latestRow, latestCol) { let idx = 0; let score = 0; if (isWinningMove(grid, who, latestRow, latestCol)) { return 1000000; } for (let row = 0; row < ROWS; row++) { for (let col = 0; col < COLS; col++) { if (grid[idx] !== who) { idx++; continue; } if (getTile(grid, row - 1, col - 1) === who) { score++; } if (getTile(grid, row - 1, col + 1) === who) { score++; } if (getTile(grid, row + 1, col - 1) === who) { score++; } if (getTile(grid, row + 1, col + 1) === who) { score++; } if (getTile(grid, row - 1, col) === who) { score++; } if (getTile(grid, row + 1, col) === who) { score++; } if (getTile(grid, row, col - 1) === who) { score++; } if (getTile(grid, row, col + 1) === who) { score++; } if (getTile(grid, row, col) === who) { score++; } idx++; } } return score;}function evaluateBoard(grid, you, opponent, latestRow, latestCol) { return evaluatePlayerBoard(grid, you, latestRow, latestCol) - evaluatePlayerBoard(grid, opponent, latestRow, latestCol);}const exampleBoard = [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 2, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,];minimax(exampleBoard, 2, Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER, COMP, -1, -1);console.log(bestMove);
您可以运行上面的代码片段,会发现输出的20
而不是11
,尽管11
显然是更好的选择,因为它会导致立即获胜。
回答:
存在几个问题:
-
使用
const val = evaluatePlayerBoard(board, player, player === COMP ? HUMAN : COMP, latestRow, latestCol);
时,您传递了两个玩家参数,而该函数只期望一个玩家参数。因此,函数会误解这些参数,导致结果无效。 -
可能与上述问题相关:您有一个
evaluateBoard
函数,从未被调用,但它确实期望两个玩家参数。我猜您原本打算在minimax
中调用这个函数。 -
尽管
evaluateBoard
返回的分数是相对于第一个玩家参数的(正值表示更好),但由于您有最大化玩家和最小化玩家,分数的符号不应由函数的参数动态决定,而应“硬编码”,以便COMP始终获得正分数,HUMAN获得负分数。因此,evaluateBoard
实际上不应接收任何玩家参数,只需第一次调用evaluatePlayerBoard
时使用COMP
作为参数,第二次调用时使用HUMAN
作为参数。 -
minimax
调用isWinningMove
时使用了错误的玩家参数。它应该是上一个移动的对手,因为传递的参数是上一个移动。 -
初始
depth
值为2时,您只允许COMP移动一次,然后HUMAN回击一次。然后您评估位置。此时还没有胜利。您应该从至少3的depth
值开始。 -
由于
bestMove
是全局变量,您有时会得到更深层次的COMP移动的最佳移动,因为无论深度如何,您都会覆盖它。但这不是您想要识别的移动。最佳做法是不使用全局变量来做这件事。相反,让minimax
返回找到的值和对应的移动。您可以将它们组合在一个数组(或普通对象)中,像这样:return [maxEval, bestMove]
。这意味着您需要在几个地方更改代码:minimax
中的所有return
语句现在都应返回一个数组,并且所有对minimax
的调用都应期望返回一个数组,并选择他们感兴趣的部分(值或移动)。 -
当
minimax
看到深度为零,并通过调用isWinningMove
检测到胜利时,它总是返回1000000,但如果上一个移动是由HUMAN执行的,它应该返回-1000000。因此,将此逻辑移到if
和else
块中。 -
这不是一个问题,但只需多加一行代码,
minimax
也可以返回HUMAN
作为初始调用者的最佳移动。我会加上它。