FE.GAME-五子棋AI博弈算法实践

最终目标 使用nodejs实现控制台人机对战五子棋,人先着,一方赢后停止交互。(如下图gif)
FE.GAME-五子棋AI博弈算法实践
文章图片

程序主要构成部分 本篇按由易到难的顺序分别讲述以下主要部分的实现

  • drawBoard 棋盘局面绘制
  • isEnd 判断该局结束
  • getMark 当前局面评分
  • abnegamax 一个minimax算法,用于双方对弈的决策树搜索
drawBoard 棋盘局面绘制 通过映射 1: '●', '-1': '○', 0: '+'局面绘制输出到控制台
function drawBoard(board, cursor) { if (typeof board === 'string') board = board.replace(/(\d{16})/g, "$1,").slice(0, -1).split(',').map(v => v.split('').map(v => v == 2 ? '-1' : v)) console.log((cursor ? "人" : "机") + "0 1 2 3 4 5 6 7 8 9 A B C D E F") for (let i = 0; i < board.length; i++) console.log((i > 9 ? i : ' ' + i) + ' ' + board[i].map((v, j) => { if (cursor && cursor.x === j && cursor.y === i) return '◎' return ({ 1: '●', '-1': '○', 0: '+' })[v] }).join('')) }

人着子时,通过方向键控制'◎'代表将落子的位置,当按回车时确定落子
process.stdin.on('keypress', (str, key) => { if (key.name === 'return') {} else { switch (key.name) { case 'up': cursor.y = cursor.y - 1; break; case 'down': cursor.y = cursor.y + 1; break; case 'left': cursor.x = cursor.x - 1; break; case 'right': cursor.x = cursor.x + 1; break; } cursor.x = Math.min(15, Math.max(0, cursor.x)) cursor.y = Math.min(15, Math.max(0, cursor.y)) drawBoard(chessBoard.join(''), cursor) }

isEnd 判断该局结束 【FE.GAME-五子棋AI博弈算法实践】每当落子后,通过统计当前落子位置为起点的4条线(横竖撇捺)是否有五子连珠
function isEnd(x, y, chessBoard) { let vect = [[-1, 0], [-1, 1], [0, 1], [1, 1]] let qi = chessBoard[y][x] for (let i = 0; i < 4; i++) { let a = 1; let b = 1; while (chessBoard[y + vect[i][0] * a] && chessBoard[y + vect[i][0] * a][x + vect[i][1] * a] === qi) a++ while (chessBoard[y - vect[i][0] * b] && chessBoard[y - vect[i][0] * b][x - vect[i][1] * b] === qi) b++ if (a + b > 5) return true } return false; }

getMark 当前局面评分 遍历每个格子的4条线(横竖撇捺)的连子数评分求和作为局面分
打分参考:
连子 得分 连子 得分
活5 1<<16 眠5 1<<15
活4 1<<12 眠4 1<<11
活3 1<<8 眠3 1<<7
活2 1<<6 眠2 1<<5
其他 1
function getScore(cnt, flag) { flag = flag ? 0 : 1; switch (cnt) { case 5: return 1 << (15 + flag) case 4: return 1 << (11 + flag) case 3: return 1 << (7 + flag) case 2: return 1 << (5 + flag) default: return 1 } }

minimax 决策算法相关 可以直接看视频 https://www.bilibili.com/vide... 学习
以下为nodejs实现描述
关于节点的数据结构
Nod类的属性 属性对应的描述
board 局面信息
cur 刚才最后落子的一方
nxt 接下来要着子的一方
deep 当前节点深度
pos 记录落子位置
mark 当前局面评分
children() 子节点
class Nod { constructor({ board, cur, nxt, deep, pos }) { this.board = board; this.cur = cur; this.nxt = nxt; this.deep = deep; this.pos = pos; this.mark = getMark(this.board); } children() { const arr = []; const reg = /0/g; while (reg.exec(this.board) != null) { arr.push(new Nod({ board: this.board.slice(0, reg.lastIndex - 1) + this.nxt + this.board.slice(reg.lastIndex), cur: this.nxt, nxt: this.cur, deep: this.deep + 1, pos: [...this.pos, reg.lastIndex - 1] })); } return arr; } }

minimax
又名极小化极大算法,是一种找出失败的最大可能性中的最小值的算法。
这个算法就是一个树形结构的递归算法,每个节点的孩子和父节点都是对方玩家,所有的节点被分为极大值(我方)节点和极小值(对方)节点。
也就是每次对方玩家都做最佳决策的情况下的决策路径下,我方依旧是优势最大的那个
function minimax(node, MAXDEEP) { if (node.deep >= MAXDEEP) return node; let arr = node.children().map(v => minimax(v, MAXDEEP)); if (node.deep % 2)//min return arr.sort((a, b) => a.mark - b.mark)[0] else return arr.sort((a, b) => b.mark - a.mark)[0] }

negamax
"负极值(Negamax)算法"是在"极大极小值(MiniMax)算法"提出近20年后才做一个小改进,在程序功能和效率上是没有区别的... 唯一不同之处是前者更看起来简洁(后者一会儿取极大,一会儿取极小).你可以发现NegaMax在传递参数时用-alpha,来起到反向取极大(也就是负极大值)的作用,因此不需要一次判断取极大,一次判断取最极小,实际上它也是在"正极大"和"负极大"间相互交替进行,原理是一样而实现手法不同。
function negamax(node, MAXDEEP) { if (node.deep >= MAXDEEP) return node; let arr = node.children(); let bestV = arr.map(v => { const n = negamax(v, MAXDEEP); n.mark = -n.mark return n; }).sort((a, b) => b.mark - a.mark)[0]; return bestV; }

minimax做alpha-beta剪枝
在上述的极大极小算法中,MIN和MAX过程将所有的可能性省搜索树,然后再从端点的估计值倒推计算,这样的效率非常低下。而α-β算法的引入可以提高运算效率,对一些非必要的估计值进行舍弃。
其策略是进行深度优先搜索,当生成结点到达规定深度时,立即进行静态估计,一旦某一个非端点的节点可以确定倒推值的时候立即赋值,节省下其他分支拓展到节点的开销。
const MAXN = 1 << 28; const MINN = -MAXN; function abminimax(node, MAXDEEP, a, b) { if (node.deep >= MAXDEEP) return node let arr = node.childrenit(); let bestV; if (node.deep % 2) {//min bestV = { mark: MAXN }; for (let child of arr) { const val = abminimax(child, MAXDEEP, a, b) if (val.mark < bestV.mark) { bestV = val; b = bestV.mark if (a >= b) break; } } } else { bestV = { mark: MINN }; for (let child of arr) { const val = abminimax(child, MAXDEEP, a, b) if (val.mark > bestV.mark) { bestV = val; b = bestV.mark if (a >= b) break; } } } return bestV; }

negamax做alpha-beta剪枝
结合两者优势,代码精简且剪枝优化
const MINN = -(1 << 28); const MAXDEEP = 2; function abnegamax(node, a, b) { if (node.deep >= MAXDEEP) return node let arr = node.childrenit(); let bestV = { mark: MINN }; for (let child of arr) { const val = abnegamax(child, -b, -Math.max(a, bestV.mark)); val.mark = -val.mark if (val.mark > bestV.mark) { bestV = val; if (bestV.mark >= b) break; } }return bestV; }

源码 https://github.com/Seasonley/...

    推荐阅读