【问题标题】:Minimax algorithm for connect 4 producing a losing move连接 4 的 Minimax 算法产生一个失败的举动
【发布时间】:2021-05-09 05:40:54
【问题描述】:

当深度设置为 4 时,该算法似乎产生了正确的移动,但当我将其增加到 5 时,它出乎意料地变得更糟。在这种特殊情况下,它建议第 0 列是下一个最佳举措,而我相信第 3 列是。我很可能不完全理解极小极大算法,所以我请求您帮助解决这个问题,因为我已经尝试了几天但没有成功。任何提高代码可读性的建议都将不胜感激。

这里是游戏的链接:http://connect4.getforge.io/ - 原谅糟糕的用户界面(wip)。默认为4级深度,增加AI_DEPTH时请观察玩法差异。

这是网格,轮到 AI 扮演 G(最大化玩家)。

 ''  '' ''   ''  ''  '' ''
 ''  '' ''   ''  ''  '' ''
 ''  '' ''   ''  ''  '' ''
 ''  '' 'G'  'R' 'G' '' 'G'
 'G' '' 'R'  'R' 'R' '' 'R'
 'G' '' 'R'  'R' 'G' '' 'G'

这是从我的项目中提取的代码:

const GRID_ROW_COUNT = 6;
const GRID_COL_COUNT = 7;
const GRID_ROW_MID = 3;
const GRID_COL_MID = 3;

const WIN_SCORE = 1000;

const rotateGrid = grid => grid.reduce((newGrid, gridRow) => {
  return newGrid.map((column, i) => column.concat(gridRow[i]));
}, [...Array(grid[0].length)].map(_ => []));

function* getValidMoves(grid, player) {
  for(let col = 0; col < grid[0].length; col++){
    for(let row = grid.length; row > 0; row--){
      if(!grid[row - 1][col]){
        const tempGrid = JSON.parse(JSON.stringify(grid));
        tempGrid[row - 1][col] = player;
        yield [tempGrid, col];
        break;
      }
    }
  }
}

const isDrawn = function(grid){
  for(let row = GRID_ROW_COUNT; row > 0; row--){
    if(grid[row - 1].filter(Boolean).length < GRID_COL_COUNT){
      return false;
    }
  }
  return true;
}

const countInRow = (target, row, index, count) => {
  if(count == 0 || !row[index] || row[index] != target){
    return index;
  }
  return countInRow(target, row, index - 1, count - 1);
}

const countInDiagonal = (target, grid, row, col, count) => {
  const colModulus = Math.abs(col);
  if(count == 0 || row < 0 || !grid[row][colModulus] || grid[row][colModulus] != target){
    return row;
  }
  return countInDiagonal( target, grid, row - 1, col - 1, count - 1 );
};

const countInCounterDiagonal = (target, grid, row, col, count) => countInDiagonal(target, grid, row, -col, count); 

function scoreGridPosition(grid, player, count = 4){
  let score = 0; 
  
  function checkWinOnHorizontals(grid, count){
    const GRID_ROW_COUNT = grid.length;
    const GRID_COL_COUNT = grid[0].length;    
    const GRID_COL_MID = player ? 0 : Math.floor(GRID_COL_COUNT/2);

    for(let row = GRID_ROW_COUNT - 1; row >= 0; row--){
      for(let col = GRID_COL_COUNT - 1; col >= GRID_COL_MID; col--){
        const cell = grid[row][col];
        if(!cell){ continue; }

        const colIndex = countInRow(cell, grid[row], col - 1, count - 1);
        
        if(col - colIndex == count){ return WIN_SCORE; }

        if(player){
          const weight = col - 1 - colIndex;
          if(cell == player){
            score += weight;
          } else {
            score -= weight * 2;         
          }
        } 

        col = colIndex + 1;
      }
    }
    return 0;
  }

  const checkWinOnVerticals = (grid, count) => checkWinOnHorizontals(rotateGrid(grid), count);

  function checkWinOnDiagonals(grid, count){
    const _GRID_ROW_MID = player ? 0 : GRID_ROW_MID;

    for(let row = GRID_ROW_COUNT - 1; row >= _GRID_ROW_MID; row--){
      for(let col = GRID_COL_COUNT - 1; col >= 0; col--){
        const cell = grid[row][col];
        if(!cell){ continue; }
        
        let rowIndexL = row, rowIndexR = row;
        
        if(col >= GRID_COL_MID){
          rowIndexL = countInDiagonal(cell, grid, row - 1, col - 1, count - 1);
        }
        if(col <= GRID_COL_MID){
          rowIndexR = countInCounterDiagonal(cell, grid, row - 1, col + 1, count - 1);
        }
        
        if(row - rowIndexL == count || row - rowIndexR == count){ return WIN_SCORE; }

        if(player){
          const weight = (row - rowIndexL) + (row - rowIndexR);
          if(cell == player){
            score += weight
          } else {
            score -= weight;
          }
        }
      }
    }
    return 0;
  }
    
  return [
    checkWinOnHorizontals(grid, count) ||
    checkWinOnVerticals(grid, count) ||
    checkWinOnDiagonals(grid, count),
    score
  ];
}

const alphaBetaAI = (grid, depth = 5, alpha = -Infinity, beta = Infinity, isMaxPlayer = true) => {
  let value = isMaxPlayer ? -Infinity : Infinity;
  let move = null;

  if(isDrawn(grid)){ return [0, move]; }

  const player = isMaxPlayer ? 'G' : 'R';
  const [terminalScore, score] = scoreGridPosition(grid, player);
  
  if(terminalScore){
                       // -1000            1000  
    return [isMaxPlayer ? -terminalScore : terminalScore, move]
  }
  if(depth == 0){ return [score, move]; }

  if(isMaxPlayer){
    for(let [newGrid, column] of getValidMoves(grid, player)){
      let [tempVal] = alphaBetaAI(newGrid, depth - 1, alpha, beta, !isMaxPlayer);
      if(tempVal > value){
        value = tempVal;
        move = column;
      }
      alpha = Math.max(value, alpha);
      if(beta <= alpha){ break; }
    }
  } else {
    for(let [newGrid, column] of getValidMoves(grid, player)){
      let [tempVal] = alphaBetaAI(newGrid, depth - 1, alpha, beta, !isMaxPlayer);
      if(tempVal < value){
        value = tempVal;
        move = column;
      }
      beta = Math.min(value, beta);
      if(beta <= alpha){ break; }
    }
  }
  return [value, move];
}

// here is the grid
let g = [
  ['', '', '', '', '', '', ''],
  ['', '', '', '', '', '', ''],
  ['', '', '', '', '', '', ''],
  ['', '', 'G', 'R', 'G', '', 'G'],
  ['G', '', 'R', 'R', 'R', '', 'R'],
  ['G', '', 'R', 'R', 'G', '', 'G']
];

console.log('Move: ', alphaBetaAI(g)[1]); // 0 - I was expecting 3

【问题讨论】:

  • 也许它意识到自己赢不了。 R 最多在四步中获胜。
  • 我明白了,但是我不知道为什么当深度增加到5时它会变得更糟。

标签: javascript artificial-intelligence minimax alpha-beta-pruning


【解决方案1】:

正如Ouroborus 所指出的,在深度5 处,无论它采取什么行动都会失败。所以现在它选择了您可能的移动列表中的第一个移动,因为所有结果都返回 -1000。

如果您希望它始终找到最长的输球路线,那么您需要在输球时返回-1000 + depth,如果您赢了,则需要返回1000 - depth。那么你的 AI 总是会选择最长的失败路线(如果有超过 1 种获胜方式,那么最快的获胜路线)。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-06
    • 2017-05-05
    • 1970-01-01
    • 2019-06-10
    • 1970-01-01
    相关资源
    最近更新 更多