欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

五子棋AI算法简易实现(四)

程序员文章站 2024-03-16 22:26:16
...

电脑AI篇


(2)生成落子位置函数

这个部分我把它分成了两个函数:
1. 找出双方所有可能下子的位置(generateAllNextPossibleMove函数)
2. 在这些位置中进行挑选,选出能够产生更大优势的下子位置,减少博弈树搜索节点的次数(pointsFilter函数)


2.1 找出双方所有可能下子的位置

这个函数用于找出相邻两格以内可以下棋的位置

x2 0 x2 0 x2
0 x1 x1 x1 0
x2 x1 C x1 x2
0 x1 x1 x1 0
x2 0 x2 0 x2

       将上面的表格比作棋盘,假设当前有棋子的位置为 C(表格中心处,颜色无论黑白,均可),那么下一步可能下棋的位置就是标有x1(表示相邻一格)或 x2(表示相邻两格) 所在的格子,不可能的位置为 0 所在的格子,因此,只要标有 x1 或 x2 的格子上(对应的棋盘位置上)没有放置棋子,那么我们都可以把它当做下一步双方可能下子的位置。

let UtilMethods = {
    // 检测当前位置是否无棋子
    isPositionEmpty(board, rowPos, colPos){
        return (board[rowPos][colPos] === Constants.Chesspiece.EMPTY);
    },
    // 检测当前位置是否有棋子
    isFilled(board, rowPos, colPos){
        return (board[rowPos][colPos] === Constants.Chesspiece.WHITE || 
            board[rowPos][colPos] === Constants.Chesspiece.BLACK);
    },
    // 当前位置相邻一格,有空位
    hasOneStepNeighbour(board, rowPos, colPos){
        // Compare to Number.isInteger, isNaN and typeof, i think this is the fastest method
        return UtilMethods.isFilled(board, rowPos-1, colPos-1) ||
            UtilMethods.isFilled(board, rowPos-1, colPos) ||
            UtilMethods.isFilled(board, rowPos-1, colPos+1) ||
            UtilMethods.isFilled(board, rowPos, colPos-1) ||
            UtilMethods.isFilled(board, rowPos, colPos+1) ||
            UtilMethods.isFilled(board, rowPos+1, colPos-1) ||
            UtilMethods.isFilled(board, rowPos+1, colPos) ||
            UtilMethods.isFilled(board, rowPos+1, colPos+1);
    },
    // 当前位置相邻两格,有空位
    hasTwoStepNeighbour(board, rowPos, colPos){
        return UtilMethods.isFilled(board, rowPos-2, colPos-2) ||
            UtilMethods.isFilled(board, rowPos-2, colPos) ||
            UtilMethods.isFilled(board, rowPos-2, colPos+2) ||
            UtilMethods.isFilled(board, rowPos, colPos-2) ||
            UtilMethods.isFilled(board, rowPos, colPos+2) ||
            UtilMethods.isFilled(board, rowPos+2, colPos-2) ||
            UtilMethods.isFilled(board, rowPos+2, colPos) ||
            UtilMethods.isFilled(board, rowPos+2, colPos+2);
    }
}

有了上述的判断方法,我们就很容易得到所有可能下子的位置

let Constants = {
    ChessBoard:{
        ROW_NUM:15,
        COL_NUM:15,
    },
    Chesspiece:{
        BLACK: 1,
        WHITE: 0,
        EMPTY: 'e',
        BORDER: 'b'
    }
};

generateAllNextPossibleMove = function(wrappedBoard, color){
    let oneStepNeighbours = [],
        twoStepNeighbours = [];

    let rowEnd = Constants.ChessBoard.ROW_NUM + 2,
        colEnd = Constants.ChessBoard.COL_NUM + 2;

    for(let i = 2; i < rowEnd; i++){
        for(let j = 2; j < colEnd; j++){           
            if(UtilMethods.isPositionEmpty(wrappedBoard, i, j)){
                if(UtilMethods.hasOneStepNeighbour(wrappedBoard, i, j)){
                    oneStepNeighbours.push([i-2, j-2]);
                }else if(UtilMethods.hasTwoStepNeighbour(wrappedBoard, i, j)){
                    twoStepNeighbours.push([i-2, j-2]);
                }
            }
        }
    }

    return [...oneStepNeighbours, ...twoStepNeighbours];
}

       这种生成下一步落子位置的方法虽然简单易懂,但是在运行时效率并不高,原因是它会产生大量的无用落子位置,大大增加了博弈树中每一层需要搜索的节点数,影响了minimax算法和AlphaBeta剪枝算法的效率。因此,我们需要对这些位置进行一个筛选,根据落子后对当前棋盘和双方局面产生的影响的程度决定待筛选的下子位置被选中的优先级,从而达到减少搜索节点数,提高运行效率的目的。实现这个目的的函数就是第二部分中的pointsFilter函数,这种方法又被称为启发式搜索,我将在下一篇博客进行介绍。

项目地址:https://github.com/huangzhutao/Gomoku.git