ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

五子棋AI实战:从极小极大搜索到α-β剪枝的博弈算法解析

五子棋AI实战:从极小极大搜索到α-β剪枝的博弈算法解析 简介面向网页游戏与AI算法学习者的五子棋人机对战资源基于前端网页技术实现完整可运行的对弈界面。其核心AI运用博弈树、极大极小值搜索与α-β剪枝能够在有限算力下模拟多步落子并作出较优决策适合对人工智能入门、游戏策略设计感兴趣的开发者参考学习。资源包共7个文件包含页面结构、样式表、两个逻辑脚本以及少量图片素材整体仅73KB轻量易读便于二次修改。已有24089人浏览学习代表该资源在同类内容中有较高关注度。通过项目代码可以直观看到AI算法从局面评估、树形搜索到最终落子的完整流程并理解剪枝技术如何在不损失决策质量的前提下大幅降低搜索开销对掌握游戏AI的实战落地很有帮助。1. 网页版五子棋AI一个zip包里的博弈逻辑解压gobang-master.zip里面是AI.js、index.js、index.html、index.css、img和几张图片。没有构建工具没有依赖安装双击index.html就能在浏览器里和它下五子棋。这个项目是典型的人工智能大作业样本用html搭界面用JavaScript实现核心决策。它采用的不是神经网络而是更直接可解释的搜索决策——博弈树、极小极大值搜索、α-β剪枝和评估函数。AI的每步棋都来自对后续局面的穷举和剪枝而不是固定棋谱。接下来我会从算法原理开始一步步拆解AI.js中的代码路径最后给出验证AI棋力和调整搜索策略的实用技巧。2. 博弈树与极小极大搜索让AI先想两步2.1 五子棋的局面为什么适合用博弈树建模五子棋每一步落子都会把棋盘从一个状态推进到下一个状态。从当前状态出发所有合法落子构成子状态继续递归就形成一棵博弈树。AI需要在这棵树里找到一条“无论对手怎么下最终结果都不差”的路径。理论上只要这棵树完全展开AI就能直接找到必胜着法。但15x15棋盘的合法落子数太多无法展开到终局。实际做法是搜索到固定深度然后用评估函数给局面打分。这个“固定深度打分”的方案就是极小极大值搜索的基本框架。2.2 用JavaScript实现最小化与最大化交替搜索下面是一份最基础的极小极大搜索实现gobang-master里的AI逻辑本质上就是它加上剪枝和启发式裁剪// board: 15x15二维数组0空位1黑子2白子 // 约定AI执黑(1)玩家执白(2) function minimax(board, depth, isMaximizing) { const winner checkWinner(board); if (winner ! 0 || depth 0) { return evaluate(board); // 对AI有利为正对玩家有利为负 } const moves getLegalMoves(board); if (moves.length 0) return 0; if (isMaximizing) { // AI要最大化局面评分 let bestScore -Infinity; for (const move of moves) { board[move.row][move.col] AI_PIECE; // 落子 bestScore Math.max(bestScore, minimax(board, depth - 1, false)); board[move.row][move.col] EMPTY; // 回溯 } return bestScore; } else { // 玩家要最小化局面评分 let bestScore Infinity; for (const move of moves) { board[move.row][move.col] HUMAN_PIECE; bestScore Math.min(bestScore, minimax(board, depth - 1, true)); board[move.row][move.col] EMPTY; } return bestScore; } }这段代码里isMaximizing为true时是AI的回合它会尝试所有棋步挑一个评分最大的为false时是玩家的回合AI假设玩家会挑最差的落着所以取评分最小。depth每递归一层就减1表示双方总共还能考虑多少手棋。比如depth4意味着AI和玩家各思考2步。调用入口通常是const bestScore minimax(board, 4, true);但注意这个函数只返回评分不返回具体落子坐标。要得到落子需要在外面套一层循环逐个试棋把评分最高的那步记下来。这也是AI.js中getBestMove要做的事。2.3 全盘搜索的代价先算清这一笔账如果完全不做裁剪在15路棋盘上每一层都有大量合法空位。理论节点数按乘法增长搜索深度不剪枝节点数约可执行性1225瞬间25.0万瞬间31124万秒级425亿分钟级55600亿不可行这里的深度是“双方合计的落子层数”。深度3还能勉强接受深度4开始就很慢必须引入剪枝。注意实际节点数会因为胜负提前返回而减少但量级不会变。调试时建议从深度2开始逐步增加观察耗时增长。3. α-β剪枝砍掉不会影响结果的分支3.1 α、β两个阈值怎么决定剪枝时机极小极大搜索之所以慢是因为它检查了大量“不影响最终决策”的分支。α-β剪枝通过维护两个阈值来避免这些无效计算。α代表当前搜索路径上MAX方AI已经能保证的最低评分β代表MIN方玩家已经能保证的最高评分。初始时α为负无穷β为正无穷。搜索过程中如果某个节点返回的评分已经超出[α, β]区间说明它不可能被双方接受就提前终止。更直观地说当AI发现自己已经有一条稳定能拿到100分的路而另一个分支上对手可以把局面压到60分AI就没有必要继续展开那个分支了。剪枝条件就是alpha beta。3.2 带剪枝的alphabeta函数下面是在极小极大代码基础上加入α-β剪枝的版本function alphabeta(board, depth, alpha, beta, isMaximizing) { const winner checkWinner(board); if (winner ! 0 || depth 0) { return evaluate(board); } const moves getOrderedMoves(board); // 经过启发式排序的候选点 if (isMaximizing) { let bestScore -Infinity; for (const move of moves) { board[move.row][move.col] AI_PIECE; bestScore Math.max(bestScore, alphabeta(board, depth - 1, alpha, beta, false)); board[move.row][move.col] EMPTY; alpha Math.max(alpha, bestScore); if (beta alpha) break; // α β剪枝 } return bestScore; } else { let bestScore Infinity; for (const move of moves) { board[move.row][move.col] HUMAN_PIECE; bestScore Math.min(bestScore, alphabeta(board, depth - 1, alpha, beta, true)); board[move.row][move.col] EMPTY; beta Math.min(beta, bestScore); if (beta alpha) break; } return bestScore; } }调用方式是const score alphabeta(board, 4, -Infinity, Infinity, true);在MAX节点里每找到一个更好的bestScore就同步更新alpha在MIN节点里更新beta。一旦beta alpha说明后续分支已经不可能被采纳直接跳出循环。注意剪枝只影响搜索效率不会改变最终评分前提是候选点顺序和评估函数正确。3.3 候选点排序剪枝效率的关键α-β剪枝的收益非常依赖节点访问顺序。如果先搜索的就是好棋那么alpha和beta会快速收敛剪枝概率大增。反过来如果先搜一堆无关落点剪枝几乎不会发生。gobang-master这类网页AI通常采用一个简单的启发式只搜索“距离已有棋子不超过2格”的空位并把更靠近中心或更靠近上一手棋的位置排在前面。function getOrderedMoves(board, lastMove) { const candidates []; const limit 2; // 裁剪半径 for (let r 0; r SIZE; r) { for (let c 0; c SIZE; c) { if (board[r][c] ! EMPTY) continue; if (lastMove Math.abs(r - lastMove.row) Math.abs(c - lastMove.col) limit) { candidates.push({row: r, col: c}); } } } if (candidates.length 0) { return getAllEmptyCells(board); // 退化方案 } // 按与上一手棋的距离升序便于剪枝 candidates.sort((a, b) { const da Math.abs(a.row - lastMove.row) Math.abs(a.col - lastMove.col); const db Math.abs(b.row - lastMove.row) Math.abs(b.col - lastMove.col); return da - db; }); return candidates; }距离看过近可能导致漏掉最佳防守点位所以limit通常取2或3。配合剪枝后搜索节点数会减少一个数量级以上深度6甚至8在普通电脑上也能接受。下表是常见经验值搜索深度剪枝候选点裁剪后性能4毫秒级6百毫秒级8秒级需小心超时4. AI.js的关键实现从棋盘状态到落子坐标4.1 项目文件的分工gobang-master里没有复杂的模块系统文件职责非常清晰文件/目录职责index.html页面结构包含canvas棋盘和操作按钮index.css棋盘样式、背景色、棋子效果的视觉控制index.js监听玩家点击、绘制棋子、调用AI并渲染落子AI.js评估函数、搜索算法、返回最佳落子坐标img/棋盘背景或棋子图片资源典型交互流程是玩家点击棋盘index.js把坐标写入当前局面并绘制然后调用AI.js暴露的getBestMove(board)把返回的坐标绘制成AI落子。4.2 棋盘数据结构和胜负检测AI和UI共用同一个15x15的二维数组const SIZE 15; const EMPTY 0; const AI_PIECE 1; const HUMAN_PIECE 2; function createBoard() { return Array.from({length: SIZE}, () Array(SIZE).fill(EMPTY)); }胜负检测是每步棋后必须执行的操作方向覆盖水平、垂直和两条对角线function checkWinner(board) { const dirs [[1,0],[0,1],[1,1],[1,-1]]; for (let r 0; r SIZE; r) { for (let c 0; c SIZE; c) { const piece board[r][c]; if (piece EMPTY) continue; for (const [dr, dc] of dirs) { let count 1; for (let step 1; step 5; step) { const nr r dr * step; const nc c dc * step; if (nr 0 || nr SIZE || nc 0 || nc SIZE) break; if (board[nr][nc] ! piece) break; count; } if (count 5) return piece; } } } return EMPTY; }dirs数组中的四个方向会覆盖所有可能的连线。如果某方向的连续数达到5直接返回获胜方。注意这里只统计了正向因为当起点是连续五子的最前一个点时正向统计足够覆盖所有情况。4.3 搜索入口与UI的联动AI.js通常暴露一个GomokuAI类核心方法包装一次完整的搜索class GomokuAI { constructor() { this.maxDepth 4; this.lastMove null; } getBestMove(board) { const moves getOrderedMoves(board, this.lastMove); let bestMove moves[0]; let bestScore -Infinity; const alpha -Infinity; const beta Infinity; for (const move of moves) { board[move.row][move.col] AI_PIECE; const score alphabeta(board, this.maxDepth - 1, alpha, beta, false); board[move.row][move.col] EMPTY; if (score bestScore) { bestScore score; bestMove move; } } this.lastMove bestMove; return bestMove; } }这里maxDepth - 1是因为当前尝试的落子已经消耗了一层深度递归搜索从下一层开始。lastMove用于候选点排序也必须在上一次AI落子后更新。UI侧的事件绑定大致如下canvas.addEventListener(click, function(e) { const x e.clientX - canvas.getBoundingClientRect().left; const y e.clientY - canvas.getBoundingClientRect().top; const col Math.floor(x / CELL_SIZE); const row Math.floor(y / CELL_SIZE); if (board[row][col] ! EMPTY) return; board[row][col] HUMAN_PIECE; drawPiece(row, col, HUMAN_PIECE); if (checkWinner(board) ! EMPTY) return; const aiMove ai.getBestMove(board); if (aiMove) { board[aiMove.row][aiMove.col] AI_PIECE; drawPiece(aiMove.row, aiMove.col, AI_PIECE); } });同步搜索在深度较浅时问题不大但深度过大会卡住UI线程。常见做法是把ai.getBestMove放进setTimeout让它在用户事件处理完之后再执行至少保证棋盘先画出玩家的棋子。5. 评估函数与启发式调优让AI学会“眼光放远”5.1 评估函数是对棋形的量化打分评估函数决定AI怎么看“当前局面好不好”。五子棋评分不能只数棋子多少核心是识别各种连字棋形。常规做法是扫描棋盘上的每个落点在四个方向统计连续棋子数以及两端开放程度。棋形建议分值说明五连100000已经胜利活四50000两端开放下一手必成五连冲四10000只有一端开放需要及时堵截活三5000下一手可以变成活四眠三1000被限制的三连活二500基础发展形状实际AI.js里评估通常分为进攻分和防守分。防守权重可以略高于进攻权重因为五子棋中防守往往更被动需要提前判断。一种常见写法是function evaluate(board) { let aiScore 0; let humanScore 0; for (let r 0; r SIZE; r) { for (let c 0; c SIZE; c) { if (board[r][c] AI_PIECE) { aiScore evaluatePoint(board, r, c, AI_PIECE); } else if (board[r][c] HUMAN_PIECE) { humanScore evaluatePoint(board, r, c, HUMAN_PIECE); } } } return aiScore - humanScore * 1.1; // 1.1是防守权重 }5.2 在极小极大搜索中接入评估下面是一个简化但可运行的evaluatePoint实现它基于某个点向四个方向统计棋形function evaluatePoint(board, row, col, piece) { const dirs [[1,0],[0,1],[1,1],[1,-1]]; let totalScore 0; for (const [dr, dc] of dirs) { let count 1; let openEnds 0; // 正向统计 let r row dr; let c col dc; while (r 0 r SIZE c 0 c SIZE board[r][c] piece) { count; r dr; c dc; } if (r 0 r SIZE c 0 c SIZE board[r][c] EMPTY) openEnds; // 反向统计 r row - dr; c col - dc; while (r 0 r SIZE c 0 c SIZE board[r][c] piece) { count; r - dr; c - dc; } if (r 0 r SIZE c 0 c SIZE board[r][c] EMPTY) openEnds; if (count 5) totalScore 100000; else if (count 4) totalScore openEnds 2 ? 50000 : 10000; else if (count 3) totalScore openEnds 2 ? 5000 : 1000; else if (count 2) totalScore openEnds 2 ? 500 : 200; } return totalScore; }需要说明的是这个实现会把同一个棋形在多个点重复计分导致分数虚高。实际工程里通常只扫描连续线段而不逐点累计或者对计数结果做除重处理。理解评分逻辑后可以将它换成更精细的线段统计版。5.3 候选点裁剪与开局库第3章提到的候选点裁剪是性能基础但还不够。前几步AI往往浪费大量时间搜索空棋盘。一个优质的五子棋AI还会加入开局库保存几组经过验证的前几手稳定走法AI只需查表即可。const OPENINGS { : { row: 7, col: 7 }, // 第一手走天元 7,7|7,8: { row: 6, col: 6 }, // 白棋下在7,8后的应对 7,7|8,8: { row: 7, col: 6 } };在getBestMove开头检查开局库const key lastMovesToString(); if (OPENINGS[key]) { const m OPENINGS[key]; if (board[m.row][m.col] EMPTY) return m; }开局库能明显提升前几步效率和稳定性尤其适合应付人工智能大作业里的多人对战演示。5.4 调参建议深度、分值、裁剪范围AI的棋力不是靠单一参数而是几个因素互相影响。下面是我常用的调参顺序const CONFIG { maxDepth: 4, candidateDist: 2, scores: { five: 100000, openFour: 50000, closeFour: 10000, openThree: 5000, closeThree: 1000, openTwo: 500 }, defenseWeight: 1.1 };先从maxDepth2开始确认UI不卡顿、棋力正常再逐步升到4或6。candidateDist从2改到3会让AI看更远的棋但节点数会成倍增长。defenseWeight调高会让AI更倾向防守调低则更激进。对局测试时最好在控制台记录每步搜索耗时观察哪个参数变化引起性能突增。6. 验证AI强度的三个技巧复盘、对称、时间控制6.1 用控制台输出搜索路径调参时最怕AI“莫名其妙乱下”。可以在alphabeta的叶子节点加日志只打印最底层候选点的评分function alphabeta(board, depth, alpha, beta, isMaximizing) { const winner checkWinner(board); if (winner ! 0 || depth 0) { const score evaluate(board); if (depth 0) { // 调试用输出每个底层节点评分 } return score; } // ... }更实用的做法是打印每个候选点作为第一手时的搜索评分for (const move of moves) { board[move.row][move.col] AI_PIECE; const score alphabeta(board, depth - 1, alpha, beta, false); board[move.row][move.col] EMPTY; console.log(候选点(${move.row},${move.col}) 评分${score}); }这样能直观看到AI选择某一步的原因也能发现评分函数中明显的漏算点。6.2 对称局面测试AI不能“一手大一手小”棋盘左右镜像后AI的落子坐标也应该镜像对称。如果不对称说明搜索或评分中对方向的处理有bug。可以在测试脚本里加一个镜像函数function mirrorBoard(board) { return board.map(row [...row].reverse()); }然后分别计算原棋盘和镜像棋盘的最佳落子。原棋盘返回(r,c)镜像棋盘应返回(r, SIZE-1-c)。不一致时常见问题是评分只写了四个方向中的一部分或者候选点排序不满足对称性。6.3 迭代加深把固定深度换成时间预算固定深度的最大问题是耗时不可控。用迭代加深可以稳定控制AI思考时间同时保留深度越深棋力越强的特点function searchWithTimeLimit(board, maxTimeMs) { const start Date.now(); let bestMove null; for (let depth 1; depth MAX_DEPTH; depth) { const move searchAtFixedDepth(board, depth); if (Date.now() - start maxTimeMs) break; bestMove move; } return bestMove; }每个深度都完整搜索一遍时间不够就回退到上一个深度已经算好的结果。配合setTimeout或Web Worker可以让AI在思考时界面保持响应玩家的体验会好很多。本文还有配套的精品资源点击获取
返回列表