五子棋(五连珠)Minimax算法未能找到明显的胜利

我正在用Javascript开发一个简单的五连珠(五子棋)AI,纯粹是为了好玩,使用了Minimax算法和Alpha-Beta剪枝。我只是按照网上的教程进行操作,但不知为何,我无法让它正常工作。我认为代码中可能存在逻辑错误,因为在下面的代码中,如果AI在第二行第三列放置棋子,会形成一个分叉,但它并未被识别为bestMove

奇怪的是,如果我设置一个断点,那个位置(row: 1, col: 2)经常被识别为winningMove,但不知为何,Minimax函数从未将bestMove计算为那个位置,尽管我认为它显然应该是那个位置。也就是说,如果AI在那里放置棋子,下一回合它几乎可以立即获胜,因为这会导致多个方向的胜利。

也就是说,如果AI在白色的2号位置放置棋子,那么在下一个AI回合中,可以实现垂直胜利或水平胜利,因为人类玩家只能阻止其中一个方向的胜利:

enter image description here

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。因此,将此逻辑移到ifelse块中。

  • 这不是一个问题,但只需多加一行代码,minimax也可以返回HUMAN作为初始调用者的最佳移动。我会加上它。

Related Posts

L1-L2正则化的不同系数

我想对网络的权重同时应用L1和L2正则化。然而,我找不…

使用scikit-learn的无监督方法将列表分类成不同组别,有没有办法?

我有一系列实例,每个实例都有一份列表,代表它所遵循的不…

f1_score metric in lightgbm

我想使用自定义指标f1_score来训练一个lgb模型…

通过相关系数矩阵进行特征选择

我在测试不同的算法时,如逻辑回归、高斯朴素贝叶斯、随机…

可以将机器学习库用于流式输入和输出吗?

已关闭。此问题需要更加聚焦。目前不接受回答。 想要改进…

在TensorFlow中,queue.dequeue_up_to()方法的用途是什么?

我对这个方法感到非常困惑,特别是当我发现这个令人费解的…

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注