资讯动态

Python实现AI五子棋:极大极小值搜索与alpha beta剪枝实战

发布时间:2026/10/25 9:48:25 来源:尧图企业网站定制
简介这是一份面向Python初学者与AI算法爱好者的五子棋AI实战项目源码围绕极大极小值搜索与Alpha-Beta剪枝两大经典决策树技术展开帮助读者理解零和博弈中的搜索策略与剪枝优化思路。压缩包共12个文件约150KB以py源码为主辅以xml配置、pyc缓存、iml工程文件及doc、pdf参考文档涵盖游戏规则、AI决策、玩家交互与主程序等模块结构清晰便于按功能阅读。目前已有3017人学习下载适合作为课程设计、毕业设计或算法练手素材。读者可从中掌握评估函数设计、递归搜索实现、Alpha-Beta边界剪枝及棋局缓存优化等关键环节并借助附带的论文与说明文档加深对搜索效率与代码组织方式的理解快速搭建可人机对弈的完整五子棋程序。1. 从一盘被 AI 反杀的棋说起极大极小值搜索到底在算什么很多人第一次写五子棋 AI都会经历同一个场景棋盘画好了落子逻辑也通了结果自己随手下了十几手AI 却像睡着一样明明活三摆在眼前也不堵。问题不在界面而在「AI 到底怎么选点」这一步。基于 Python 的 AI 五子棋实现核心就是让程序在有限时间内评估未来若干步选出对自己最有利的落子。而支撑这套决策的经典算法就是极大极小值搜索配合 alpha beta 剪枝。这套方案适合两类人一类是刚学完 Python 基础、想找一个能跑通又有成就感的练手项目另一类是想理解博弈树搜索、为后面做更复杂棋类或决策系统打底的人。它不需要深度学习框架不依赖显卡一台普通笔记本就能跑。难点也不在 Python 语法而在「怎么把棋局变成分数」「怎么在爆炸的状态空间里砍掉无用分支」。下面我按自己实际写过的顺序把选型、评估函数、搜索框架、剪枝和调参一层层拆开。2. 棋盘表示与评估函数AI 的「眼睛」怎么造2.1 用二维列表还是 NumPy 数组棋盘表示决定了后面所有代码的写法。最常见的做法是用 15×15 的二维列表0 表示空1 表示黑2 表示白。优点是直观、调试方便打印出来一眼就能看懂。缺点是每次评估都要遍历大量格子纯 Python 循环在深层搜索时会明显拖慢速度。如果你已经装了 NumPy可以用np.zeros((15,15), dtypenp.int8)内存更紧凑切片和向量化统计也更快。但要注意极大极小值搜索里大量操作是「试下—评估—撤销」NumPy 的拷贝开销反而可能拖后腿。我一般会先用二维列表把逻辑跑通确认搜索深度和剪枝正确后再针对热点函数做局部优化。新手直接上 NumPy 容易在索引和拷贝上翻车血泪经验是先让 AI 能下棋再让它下得快。# 棋盘初始化15x150空 1黑 2白 BOARD_SIZE 15 board [[0] * BOARD_SIZE for _ in range(BOARD_SIZE)] def print_board(board): # 简单打印方便调试 for row in board: print( .join(str(x) for x in row))这段代码没有复杂逻辑重点在board的结构。后面所有落子、撤销、评估都围绕它展开。BOARD_SIZE单独抽出来是为了后面改棋盘大小时不用满文件找数字。2.2 评估函数把棋型翻译成分数评估函数是 AI 的「眼睛」。它把当前棋盘扫一遍统计双方形成了哪些棋型再折算成分数。五子棋常见棋型按威胁从高到低大致是连五、活四、冲四、活三、眠三、活二、眠二。分值可以设成棋型黑方分值白方分值说明连五100000100000已经赢了活四1000010000两头都能成五冲四10001000只有一头能成五活三10001000能变活四眠三100100受限的三活二100100有发展空间眠二1010受限的二实际写的时候不需要对每个格子做复杂模式匹配常见做法是扫描每条横、竖、斜线把连续同色段提取出来再根据两端是否被封判断棋型。下面是一个简化版的行扫描思路def evaluate_line(line, role): # line 是一串 0/1/2role 是当前评估的一方 score 0 count 0 for cell in line: if cell role: count 1 else: if count 5: score 100000 elif count 4: score 10000 elif count 3: score 1000 elif count 2: score 100 elif count 1: score 10 count 0 # 处理行尾连续段 if count 5: score 100000 elif count 4: score 10000 elif count 3: score 1000 elif count 2: score 100 elif count 1: score 10 return score这段代码只统计了「连续同色」的粗略分数没有区分活四和冲四。真实项目里需要额外记录连续段两端是否为空才能把活四和冲四分开。参数上连五必须远大于其他棋型之和否则 AI 可能为了堵一个活三而放弃自己成五。我一般会让连五分值至少是活四的 10 倍以上避免「赢了不赢」的玄学局面。2.3 评估整盘棋遍历四个方向单行评估只是基础整盘评估要把横、竖、两条斜线都扫一遍。常见做法是分别提取所有行、所有列、所有主对角线和副对角线对每条线调用评估函数最后把 AI 方得分减去对手得分得到当前局面的净分数。def evaluate_board(board, ai_role, human_role): total 0 # 横向 for row in board: total evaluate_line(row, ai_role) total - evaluate_line(row, human_role) # 纵向 for col in range(BOARD_SIZE): line [board[row][col] for row in range(BOARD_SIZE)] total evaluate_line(line, ai_role) total - evaluate_line(line, human_role) # 主对角线 for d in range(-BOARD_SIZE 1, BOARD_SIZE): line [board[i][i - d] for i in range(BOARD_SIZE) if 0 i - d BOARD_SIZE] total evaluate_line(line, ai_role) total - evaluate_line(line, human_role) # 副对角线 for d in range(2 * BOARD_SIZE - 1): line [board[i][d - i] for i in range(BOARD_SIZE) if 0 d - i BOARD_SIZE] total evaluate_line(line, ai_role) total - evaluate_line(line, human_role) return total这里ai_role和human_role分别代表 AI 和玩家的棋子编号。净分数为正说明 AI 占优为负说明玩家占优。搜索时 AI 会尽量让这个分数变大玩家则相反。注意对角线提取时索引边界容易写错建议先用小棋盘比如 5×5验证每条线长度是否正确再换回 15×15。3. 极大极小值搜索把「我走一步、你走一步」变成一棵树3.1 搜索树的基本结构极大极小值搜索把对局看成一棵树根节点是当前局面每个子节点代表一种落子后的新局面。AI 层希望分数越大越好玩家层希望分数越小越好。搜索到指定深度后用评估函数给叶子节点打分再逐层回传AI 层取最大值玩家层取最小值。def minimax(board, depth, is_ai, ai_role, human_role): # 到达深度限制或终局返回评估分 if depth 0 or game_over(board): return evaluate_board(board, ai_role, human_role) if is_ai: best -float(inf) for (r, c) in get_empty_cells(board): board[r][c] ai_role score minimax(board, depth - 1, False, ai_role, human_role) board[r][c] 0 best max(best, score) return best else: best float(inf) for (r, c) in get_empty_cells(board): board[r][c] human_role score minimax(board, depth - 1, True, ai_role, human_role) board[r][c] 0 best min(best, score) return bestdepth控制搜索层数is_ai标记当前该谁走。每次试下后立刻撤销保证棋盘状态可复用。get_empty_cells返回所有空位实际项目里不会真的遍历 225 个点而是只考虑已有棋子周围两格内的候选点否则第一层就展开两百多个分支深度 4 基本跑不动。3.2 候选点生成别在空棋盘上瞎搜五子棋大部分格子是空的真正值得考虑的是已有棋子附近的位置。常见做法是维护一个候选集合只把距离已有棋子不超过 2 的空白点加入搜索。def get_candidates(board, radius2): candidates set() for r in range(BOARD_SIZE): for c in range(BOARD_SIZE): if board[r][c] ! 0: for dr in range(-radius, radius 1): for dc in range(-radius, radius 1): nr, nc r dr, c dc if 0 nr BOARD_SIZE and 0 nc BOARD_SIZE: if board[nr][nc] 0: candidates.add((nr, nc)) return list(candidates)radius2是经验值。太小会漏掉跳活三的防守点太大分支数暴涨。如果发现 AI 偶尔不堵关键位置先把 radius 调到 3 试试确认是候选集问题还是评估函数问题。这个函数在每层递归里都会被调用可以缓存或增量更新但新手先保证正确性。3.3 深度与性能的取舍纯极大极小值搜索的复杂度是分支数的深度次方。假设每层 30 个候选点深度 4 就是 81 万次评估纯 Python 可能要跑好几秒。深度 5 直接上千万基本不可接受。所以实际项目里必须配合剪枝把无效分支砍掉。下面这张表是我在不同深度下的粗略体感搜索深度无剪枝耗时加 alpha beta 后棋力表现2很快很快只会看一步容易被骗3可接受很快能堵活三进攻弱4明显卡顿可接受业余新手能打5基本跑不动较慢需要优化评估函数6不可用勉强必须配合候选点裁剪我一般会把默认深度设成 4让 AI 在可接受时间内有基本攻防。如果机器性能好可以尝试 5但一定要先做剪枝和候选点限制。4. alpha beta 剪枝砍掉那些「不用看也知道没用」的分支4.1 剪枝到底在剪什么alpha beta 剪枝的核心思想是如果已经发现某个分支的结果不可能比当前已知的最优选择更好就不用继续展开这个分支。alpha 记录 AI 层当前能保证的最好分数beta 记录玩家层当前能保证的最差分数。当 alpha beta 时说明这个分支对双方都没有继续看的价值直接返回。def alphabeta(board, depth, alpha, beta, is_ai, ai_role, human_role): if depth 0 or game_over(board): return evaluate_board(board, ai_role, human_role) if is_ai: best -float(inf) for (r, c) in get_candidates(board): board[r][c] ai_role score alphabeta(board, depth - 1, alpha, beta, False, ai_role, human_role) board[r][c] 0 best max(best, score) alpha max(alpha, best) if alpha beta: break # 剪枝 return best else: best float(inf) for (r, c) in get_candidates(board): board[r][c] human_role score alphabeta(board, depth - 1, alpha, beta, True, ai_role, human_role) board[r][c] 0 best min(best, score) beta min(beta, best) if alpha beta: break # 剪枝 return bestalpha初始为负无穷beta初始为正无穷。AI 层更新 alpha玩家层更新 beta。一旦 alpha 超过 beta说明这个分支的结果已经被之前的更好选择覆盖继续搜索没有意义。剪枝效果高度依赖候选点的顺序如果先搜到好棋剪枝会砍掉大量分支如果先搜到差棋剪枝效果大打折扣。4.2 候选点排序让剪枝真正生效一个实用技巧是在展开子节点前先按「粗略威胁分」排序把可能形成活四、冲四、活三的点排在前面。这样 alpha beta 能更早遇到好棋剪枝效率明显提升。def sort_candidates(board, candidates, role): def rough_score(pos): r, c pos board[r][c] role s evaluate_board(board, role, 3 - role) board[r][c] 0 return s return sorted(candidates, keyrough_score, reverseTrue)role是当前落子方3 - role得到对手编号。这个排序本身有开销但换来的剪枝收益通常更大。如果发现加了排序反而变慢说明评估函数太重需要先简化评估或减少候选点数量。4.3 终局判断与胜负分搜索到连五时必须立刻返回一个极大或极小的分数而不是继续用评估函数。否则 AI 可能因为评估函数没给连五足够高的分而错过必胜点。def game_over(board): # 检查是否有连五 for r in range(BOARD_SIZE): for c in range(BOARD_SIZE): if board[r][c] 0: continue role board[r][c] for dr, dc in [(0,1),(1,0),(1,1),(1,-1)]: count 1 nr, nc r dr, c dc while 0 nr BOARD_SIZE and 0 nc BOARD_SIZE and board[nr][nc] role: count 1 nr dr nc dc if count 5: return True return False这个函数在每层递归都会被调用效率不高但逻辑清晰。优化做法是只在落子位置周围检查四个方向而不是全盘扫描。新手先用全盘版保证正确后面再改。5. 避坑与排查那些让我熬夜的翻车现场5.1 AI 明明能赢却去堵对方现象AI 自己已经活四下一手就能成五却跑去堵玩家的活三。 原因评估函数里活四和活三分值太接近或者连五分值不够高导致搜索回传时「赢棋」和「堵棋」分数差不多。 解决把连五分值调到远大于其他棋型之和并在game_over里对连五直接返回胜负分不走普通评估。5.2 搜索深度一加就卡死现象深度设成 4 还能跑设成 5 直接几十秒没响应。 原因候选点没有限制每层展开两百多个分支加上评估函数每次全盘扫描计算量爆炸。 解决用get_candidates限制候选点评估函数改成增量更新或只扫描落子附近必要时把深度降回 4。5.3 剪枝后棋力反而下降现象加了 alpha beta 之后AI 偶尔下出明显臭棋。 原因候选点顺序太差剪枝把正确分支提前砍掉了或者 alpha、beta 更新写反AI 层和玩家层搞混。 解决先关掉剪枝验证纯极大极小值结果确认评估和搜索逻辑正确再逐步加剪枝和排序。检查is_ai分支里更新的是 alpha玩家分支更新的是 beta。5.4 棋盘边界索引越界现象对角线提取或连五判断时报IndexError。 原因斜线遍历时没有正确限制行列范围负索引在 Python 里会绕回末尾导致逻辑错误而不是报错。 解决所有索引操作前加0 nr BOARD_SIZE判断对角线提取用列表推导时显式过滤边界。5.5 评估函数正负号写反现象AI 一直往角落下完全不进攻。 原因evaluate_board里 AI 分和玩家分减反了导致 AI 以为自己的棋对对手有利。 解决打印几个典型局面的评估分手动算一遍预期值确认 AI 方得分是加、玩家方得分是减。6. 进阶技巧用置换表和迭代加深把棋力再抬一档当基础版本跑通后最值得做的两个优化是置换表和迭代加深。置换表用字典缓存已经搜索过的局面键可以用棋盘字符串加当前深度和轮次。同一个局面在搜索树里可能通过不同顺序到达缓存后能省下大量重复计算。transposition {} def board_key(board, depth, is_ai): return (str(board), depth, is_ai) def alphabeta_with_tt(board, depth, alpha, beta, is_ai, ai_role, human_role): key board_key(board, depth, is_ai) if key in transposition: return transposition[key] # ... 原有搜索逻辑 ... transposition[key] best return best注意置换表要限制大小否则内存会持续增长。我一般设一个上限超过就清空或按简单策略淘汰。迭代加深则是从深度 1 开始搜逐步加深每次用上一层的最优走法作为下一层的首选候选点这样 alpha beta 剪枝效率更高还能在时间不够时随时返回当前最优解。另一个实用技巧是「只搜关键点」如果 AI 或玩家已经有活四直接返回胜负分不再展开。这个判断放在搜索入口能省掉大量无意义递归。最后说个我自己的习惯每次改完评估函数或剪枝逻辑先用几个固定局面做回归测试比如「AI 活四必须赢」「玩家活四必须堵」「双活三必须防」确认没有退化再继续调参。这套基于 Python 的 AI 五子棋实现真正花时间的不是写搜索框架而是反复调评估函数和验证边界。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑