当深度设置为4时,算法似乎能产生正确的移动,但当我将其增加到5时,效果意外地变差了。在这个特定情况下,它建议第0列是下一个最佳移动,而我认为应该是第3列。我可能对极小化极大算法的理解还不够全面,所以我请求您的帮助来解决这个问题,因为我已经尝试了几天都没有成功。此外,任何关于提高代码可读性的建议也将不胜感激。
这是游戏的链接:http://connect4.getforge.io/ – 请原谅这个简陋的用户界面(正在开发中)。默认深度为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 gridlet 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
回答:
正如Ouroborus指出的那样,在深度5时,无论它采取什么移动都会输。所以现在它选择了您可能移动列表中的第一个移动,因为所有结果都返回-1000。
如果您希望它总是找到最长的输棋路线,那么当输棋时,您需要返回-1000 + depth
,当赢棋时返回1000 - depth
。这样,您的AI将始终选择最长的输棋路线(如果有多个赢棋方式,则选择最快的赢棋方式)。