尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

用Python实现α-β剪枝五子棋AI:从原理到pygame实战

用Python实现α-β剪枝五子棋AI:从原理到pygame实战 简介基于α-β剪枝算法实现的五子棋人机对战小游戏面向Python学习者与博弈算法入门者演示如何将极大极小值搜索与剪枝优化落地到交互式游戏中。程序包含完整的人机对战流程支持先后手切换、胜负判定以及棋盘信息显示其中评估函数的设计与下一步候选点选取是体现“电脑智能”的关键模块。压缩包共3个文件含2个Python源文件与1个说明文本代码量精炼便于逐行阅读和二次改进整体大小仅15KB。资源发布以来已有4703人学习浏览适合课程设计、毕业设计或算法实践参考。读者可从中获得可运行的五子棋程序、博弈树剪枝实现思路以及界面交互设计细节有助于快速理解α-β剪枝在真实游戏中的应用方式。 前阵子陪家人下五子棋输多赢少气不过干脆自己写了个AI。用Python实现了基于α-β剪枝的五子棋人机对战界面层用的pygame跑起来的效果比预期强不少——中等搜索深度下已经能轻松打赢没有专门研究过棋路的朋友。这篇就把整个项目完整复盘一遍从算法原理、评估函数设计到pygame界面怎么接、参数怎么调、踩了哪些坑按实操顺序全说清楚想复现的话照着做就行。这个项目非常适合刚入门AI算法或者想练手Python项目的人。它的妙处在于五子棋规则简单但搜索空间又不小刚好能把α-β剪枝的精髓用上界面用pygame虽然简陋但足够演示“搜索算法输出的坐标”怎么变成屏幕上的一颗棋子。整个过程不用机器学习、不用GPU一台普通电脑就能跑代码量控制在几百行作为练手再合适不过。1. 项目思路与方案选型1.1 为什么选α-β剪枝而不是深度学习或蒙特卡洛市面上做棋类AI有很多路子深度学习、蒙特卡洛树搜索、还有最基础的极大极小搜索。深度学习那套开源框架倒是很成熟但你要准备大量对局数据或者自己和自己下棋生成数据训练一轮的等待时间、调参的折腾程度对一个小项目来说性价比很低。蒙特卡洛树搜索MCTS也是好方案但它的随机模拟策略不经过专门调优棋力其实一般还要处理置信区间、节点复用这些细节。α-β剪枝是极大极小搜索的优化版本逻辑非常简单双方轮流落子AI在搜索树上展开所有可能的走法对手也会选择对AI最不利的应对AI只能在这些最坏结果里挑一个最好的。看起来是暴力搜索但通过α-β剪枝能裁掉大量肯定不优的分支深度在4~6层时已经能表现出不错的棋力。而且整个过程完全可解释——每一步AI都能告诉你凭什么选这个点出了问题也容易查这种“知道它在干什么”的感觉对学习算法来说很重要。1.2 整体架构界面、逻辑、AI三层分开拿到这个项目我第一件事不是写代码而是把模块在脑子里拆清楚。拆成三层界面层pygame负责画棋盘、画棋子、接收鼠标点击、刷新画面。这一层只听用户的输入不关心任何AI逻辑。逻辑层管理棋盘数据结构、判断落子合法性、判断胜负。这层是棋盘的心脏AI和界面都依赖它。AI层走法生成 评估函数 α-β搜索。输入棋盘状态输出一个落子坐标。这种分层最大的好处是调试效率大幅提升。我可以在纯命令行环境下先验证AI逻辑不启动pygame直接打印ASCII棋盘让AI和AI对战看效果。因为界面和AI完全解耦所以很多bug在终端里就暴露了不需要反复重启图形窗口。实际开发中这个决策帮我省了至少三分之一的时间。2. 核心原理极大极小搜索与α-β剪枝2.1 博弈树视角AI和玩家的对抗模型五子棋是双人完全信息博弈AI和玩家轮流落子彼此都看得到棋盘上所有信息。要决定当前这一步AI会假设自己下一步之后对手会选择对他最不利的应对再之后AI又要选择对自己最有利的再往后对手又要选择对AI最不利的……如此交替构成一棵博弈树。在代码里这种交替用最大值层和最小值层表示轮到AI走棋的节点是最大值层它要从所有子节点中挑得分最高的轮到玩家走棋的节点是最小值层它要从所有子节点中挑得分最低的因为玩家想让AI输。整棵树的评估从叶子节点开始回溯到根节点根节点选中的那条分支就是AI的落子。这个模型想明白了再看复杂度就清楚了。15×15的棋盘第一层候选位置约200第二层约199完全不剪枝的情况下深度4的搜索就要评估200×199×198×197个局面算出来超过15亿。哪怕每个局面评估只要1微秒也要好几分钟完全没法玩。所以必须做两件事缩小每层的候选数量以及用剪枝砍掉没必要搜索的分支。2.2 α、β到底怎么剪用上下界裁掉无用搜索α-β剪枝的本质是维护两个值α是最大值层已经能保证得到的最低分数β是最小值层已经能保证的最高分数。当访问某个节点时把这个节点看成一个区间[α, β]意思是无论这个节点下面的子树怎么搜最大值方最终得到的分数不可能低于α最小值方不可能允许分数高于β。如果在某个最小值层的子节点里它的返回值已经小于等于α那说明对手在上层已经有更优选择当前这个分支搜下去已经没意义了——直接剪掉。同理在最大值层的子节点里如果返回值大于等于β也直接剪掉。剪枝条件在代码里就两行判断但它的威力完全取决于走法顺序——先搜“最有希望的分支”能让α、β快速收敛剪掉的分支就多。如果每次都先搜烂分支α、β半天不更新剪枝效果就很差速度可能差几个数量级。这个细节我在调优时反复踩坑后面会专门说。2.3 启发式评估函数凭什么给一个棋局打分搜索树的底层需要给每个叶子局面算一个分数这个打分函数就是启发式评估函数。设计的核心原则是只看局部棋型的威胁程度。一个局面里AI有活三、对手也有活三谁的威胁更大只要给不同棋型赋不同分值然后统计双方棋型总分相减就能量化。我采用的棋型评分思路是遍历棋盘上每个非空棋子沿横、竖、两个对角线共四个方向统计以它为起点的连续同色棋子数以及两端被空位还是被对方棋子/边界堵住。按这个信息把棋型归为五连、活四、冲四、活三、眠三、活二、眠二等几个档次套用经验分值累加。实际效果看下来AI的棋风很大程度上由这张评分表决定后面我会贴出我的分值表供参考。3. 从零撸代码三层实现的关键细节3.1 走法生成先把候选分支缩小到可控范围15×15棋盘上全盘搜索不现实所以第一步是筛选候选点。我采用的规则是只考虑距已有棋子不超过2格即周围5×5范围内有棋子的空位。这个规则的依据是五子棋落子通常紧贴战场远离棋子的点既不构成威胁也不参与防守搜索了纯属浪费。筛选完之后候选点数量从225个降到平均80~120个已经缩小了一个量级。但还不够还需要给候选点排序把“最重要的点”排前面这样α-β剪枝才能尽早找到强分支。我的排序函数很简单统计每个空位周围一圈的棋子总数如果同时包含双方棋子和已方棋子就给予更高的启发分。实测下来把高威胁点排在前面剪枝效率能提升一大截同样深度下耗时常常差出一倍以上。def get_candidate_moves(board.board_state, last_move, max_candidates80): moves set() # 遍历棋盘只找周围5x5内有棋子的空位 for row in range(BOARD_SIZE): for col in range(BOARD_SIZE): if board.board_state[row][col] ! EMPTY: continue if has_neighbor(board.board_state, row, col, distance2): score nearby_density(board.board_state, row, col) moves.add((row, col, score)) # 按启发分从高到低排序截断前 max_candidates 个 moves sorted(moves, keylambda x: x[2], reverseTrue) return [(r, c) for r, c, _ in moves[:max_candidates]]has_neighbor负责判断该空位周围是否存在棋子nearby_density统计一圈内的棋子分布密度。这个函数是整个搜索效率的基石这也是整个优化里性价比最高的一个操作不改变任何棋力只是调整搜索顺序速度就能快上很多。3.2 评估函数怎么写棋型对比和计分评估函数的质量决定了AI的“大局观”。我采用的方案是分别在四个方向上分析每一方同色棋子的连子分布判断属于哪种棋型累加分数最后返回AI总分减去玩家总分。棋型判断的核心步骤是这样的从某个起点向某个方向延伸数出连续相同颜色的棋子个数再检查两端的情况。两端都空着且后续空间够大才是“活”的棋型有一端被对方或边界堵住就只能算“眠”的类型。我整理了一个经验评分表这是调试过程中不断调整得到的棋型分值说明五连100000直接获胜必须给最高值活四50000两端都畅通对手无法阻挡冲四10000一端被堵成四对手必须应对活三5000下一手可以变活四压迫感极强眠三1000只能形成冲四威胁中等活二500发展潜力中盘布局的关键眠二100基本防御性棋型需要注意评估时要同时扫描AI和玩家的棋型不要把分数算漏。因为五子棋是防守和进攻并重的游戏如果只关心AI自己的棋型AI就会变成“只顾自己冲、不顾对手何时连五”的莽夫。我早期版本就是因为只算了自己一侧的分数AI经常忽略对手马上就要赢的位置被一手活四直接带走。后来改成差分评估也就是AI总评分减去玩家总评分并把防守位置也纳入候选点排序AI才开始知道“堵”的重要性。有了算分逻辑后评估函数主体如下def evaluate(board_state, ai_color, player_color): total_score 0 total_score calculate_color_score(board_state, ai_color) total_score - calculate_color_score(board_state, player_color) return total_scorecalculate_color_score内部就是双重循环扫描所有棋子往四个方向分析棋型查表累加分值。这样的实现虽然不算极致优化但配合剪枝后性能完全够用。3.3 α-β搜索主函数递归与剪枝的实现说完了评估函数现在就到了搜索主函数。这里最需要注意的是递归过程中棋盘状态的正确恢复——落子之后搜索完一定要撤销否则后面的分支会在错误的状态上继续搜索出现的bug非常隐蔽排查起来极其痛苦。INF float(inf) def alpha_beta_search(board_state, ai_color, player_color, depth, alpha, beta, is_maximizing): winner check_winner(board_state) if winner is not None or depth 0: return evaluate(board_state, ai_color, player_color) total_empty sum(row.count(EMPTY) for row in board_state) if total_empty 0: return evaluate(board_state, ai_color, player_color) # 平局 moves get_candidate_moves(board_state, None) if is_maximizing: best_score -INF for row, col in moves: board_state[row][col] ai_color score alpha_beta_search(board_state, ai_color, player_color, depth - 1, alpha, beta, False) board_state[row][col] EMPTY best_score max(best_score, score) alpha max(alpha, best_score) if beta alpha: break return best_score else: best_score INF for row, col in moves: board_state[row][col] player_color score alpha_beta_search(board_state, ai_color, player_color, depth - 1, alpha, beta, True) board_state[row][col] EMPTY best_score min(best_score, score) beta min(beta, best_score) if beta alpha: break return best_score初始调用时alpha设为负无穷beta设为正无穷。每一层判断胜负之后优先走winner is not None分支返回直接给胜负结果一个极大/极小值这样一旦某条路径能让AI赢或者让AI输搜索会立刻收敛。外层的封装函数拿到搜索返回的最大值分支对应的落子位置这个落子位置才是AI真正要走的一步。我在实现时又包了一层get_best_move它负责遍历第一层的每一个候选落子记录每个落子搜索后的分数最后选出分数最高的那一个。3.4 pygame界面从鼠标点击到落子到胜利判定pygame这部分其实没有太多花哨的东西核心就三件事初始化棋盘画面、把鼠标像素坐标换算成棋盘行列、判定胜负后显示提示。棋盘我画成15×15的网格格子边长40像素棋盘区域留一些边距。把鼠标点击的像素坐标减去边距再除以格子边长取整就得到行列号。这个坐标换算是界面层的核心也是很多人第一次写的时候容易搞混的地方。def pixel_to_board(mouse_pos): x, y mouse_pos board_x (x - MARGIN) // CELL_SIZE board_y (y - MARGIN) // CELL_SIZE return board_y, board_x胜利判定我封装成check_winner函数每次落子之后从落子点出发沿四个方向水平、竖直、主对角线、副对角线分别往两头延伸数同色棋子连续个数。只要任意方向达到5个就返回该方胜。这个方法比每次全盘扫面高效得多因为每次只需要检查刚落下的那个点周围其他区域根本不可能突然产生胜利。事件循环主体大概长这样while running: for event in pygame.event.get(): if event.type pygame.QUIT: running False elif event.type pygame.MOUSEBUTTONDOWN and not game_over: row, col pixel_to_board(pygame.mouse.get_pos()) if board[row][col] EMPTY and not is_ai_turn: board[row][col] PLAYER_COLOR draw_board(screen, board) pygame.display.flip() is_ai_turn True if is_ai_turn and not game_over: best_move get_best_move(board, AI_COLOR, PLAYER_COLOR, DEPTH) board[best_move[0]][best_move[1]] AI_COLOR draw_board(screen, board) pygame.display.flip() is_ai_turn False注意落子后要立刻调用pygame.display.flip()刷新画面走棋逻辑放在事件循环之外这样AI计算期间界面不会假死太久虽然深度大时还是会卡这是正常的。我还在draw_board函数里把“刚出棋的位置”画了一个小红框方便看AI的思路到底落在哪调试时非常有用。4. 参数调优与性能实测4.1 搜索深度和候选点上限难度的旋钮α-β剪枝项目里的“AI难度”本质上就是搜索深度。深度越小AI只看得见眼前一两步棋力就弱深度越大搜索层数越多计算时间成倍增长。我建议新手先从深度2开始跑通流程再逐步往上加。候选点上限也是一个关键参数。限制候选点可以显著提升搜索速度但限制太狠会漏掉一些关键应对。我实测下来保持80~100个候选点在普通笔记本上搜索深度初始候选点数平均单步耗时体验评价2100小于0.1秒入门难度经常犯低级错误31000.1~0.3秒反应迅速有明显棋理但也有失误41000.5~1.5秒流畅对局普通玩家很难赢5802~5秒等待感明显棋力已经很扎实这个数据是在Python 3.10 普通桌面CPU上跑的。如果深度再往上到6、7单步会超过10秒已经没法当实时游戏玩只能用来做后台分析。4.2 攻守平衡评估函数比搜索深度更影响观感很多人误以为深度越深AI越强实际测试下来评估函数的攻守倾向比深度更影响观感。我之前用纯差分评估AI在深度4的时候下得畏畏缩缩喜欢铺子但不会主动追杀。后来我把评估函数改成“进攻分值乘以1.1防守分值乘以0.95”AI立刻变得激进起来频繁做活三、冲四。但攻守系数也不能太一边倒。我试过把攻击系数拉到1.5AI变得极度贪婪只顾自己连子完全不管对手的活三结果被玩家轻松用活三变活四带走。最终我调到了攻击系数1.05防守系数0.98AI表现得有侵略性但不会忽视防守。参数这类细节非常依赖具体场景给一个推荐值比给一个绝对值更有参考意义大家务必在自己的棋盘上多下几局感受一下差异。5. 常见问题与排障实录5.1 pygame安装和运行环境那些坑很多人卡在第一步——pygame装不上。常见的报错是编译类的错误比如pip install pygame时出现error: failed to build pygame when getting requirements to build wheel。这类问题基本都是pip尝试从源码直接编译而系统里缺对应的编译工具链比如Windows上缺少Visual Studio Build Tools或者Linux上缺少Python头文件。最简单稳妥的做法是优先装预编译好的二进制wheel包pip install --upgrade pip pip install pygame --prefer-binary如果还不行可以换个镜像源再试一次。再不行检查一下Python版本建议用3.8到3.11之间的稳定版本太新的Python版本有时候会出现没有对应wheel的情况。运行环境的问题九成以上都出在这个环节不在算法本身。5.2 AI计算太慢或者“送子”问题AI慢十有八九是走法排序不到位或者评估函数太重。如果你发现深度3已经很卡先去检查候选点是不是全盘搜索——没做邻域筛选的话深度3也得几百万次评估确实卡。先加候选点生成逻辑再加走法排序速度通常会有数量级的提升。AI“送子”是什么意思就是AI突然下了一步毫无威胁的棋放着对手的活四不管。这通常是评估函数只算了AI自己的分没算对手的分导致防守位置在搜索中分数不高。把评估改成双向计算之后这个现象基本消失。还有一种隐蔽情况递归搜索时没恢复棋盘状态导致后面所有分支都在一个“加了多余棋子”的棋盘上评估结果完全错乱。这个bug排查起来非常费劲我建议在递归入口加一个断言确认落子位置的棋子颜色和预期一致能快速暴露这种问题。5.3 胜负判定和边界判断的细节还有一种新手常犯的错误胜负判定只检查一个方向。五子棋胜利可以是横、竖、左斜、右斜四个方向漏掉任何一个方向都会出现“明明连成五颗却不判胜”的尴尬情况。判断边界时尤其注意数组越界——从棋盘边缘往斜方向延伸时坐标可能跑出15×15的范围。把所有坐标判断统一封装成一个is_valid(row, col)函数在循环里每次都检查能避免绝大部分越界问题。最后再分享一个小经验AI层不要依赖pygame的任何对象所有输入输出都用纯Python的数据结构比如二维数组。这样你可以在终端里写简单的对局脚本让AI自己和自己下几十盘收集胜率数据来调参。我早期很多棋型分值的调整都是靠这种自动化对局测试来验证的。如果你也想进一步扩展这个项目可以试试置换表缓存已搜索过的局面或者加入迭代加深让AI在时间允许的情况下尽量搜得更深——这些方向我已经在陆续更新后续有机会再单独写一篇。本文还有配套的精品资源点击获取
返回列表