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

资讯详情

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

五子棋AI课程设计:从α-β剪枝到评估函数的博弈搜索实现

五子棋AI课程设计:从α-β剪枝到评估函数的博弈搜索实现 简介这是一份面向高校人工智能、计算机相关专业学生的课程设计报告完整呈现了五子棋人机对战系统从需求分析、总体设计到编码实现的全过程。报告首先介绍五子棋规则与核心需求包括人机对战、悔棋、胜负判断三大功能随后对棋盘数据m_data、清空棋盘、初始化、绘制棋子、左键消息、双缓冲绘制等关键模块逐一说明并重点剖析胜负判断Win算法给出横、竖、斜四个方向连续五子判定的核心代码。文中还展示了程序运行界面分析算法效率、界面优化空间并总结开发过程中的心得与思考对准备人工智能课程设计、毕业设计选题或复习博弈类游戏实现思路的学生有较强的参考价值。资源整体为1个PDF文档大小约358KB内容结构清晰目录完整便于快速定位阅读。截至目前已有1064人学习下载是同类设计报告中较受欢迎的一份参考资料。1. 从“人工智能课程设计报告-五子棋.pdf”看人工智能力作怎么写“人工智能课程设计报告-五子棋.pdf”这个名称背后要交付的不是一个能走通的 pygame 小游戏而是一份能解释“棋力从哪来”的人工智能大作业15 路棋盘上电脑在限定时间内完成落子同时代码、报告、答辩陈述都能自圆其说。多数人第一反应是拿深度学习读棋盘但在课程设计场景里深度模型的可解释性差、训练数据难构造反而让答辩变成盲区。更常见也更稳妥的路线是“博弈搜索 启发式评估”胜负判定、α-β 剪枝、评估函数三个模块互相独立可单测、可调参。下面顺着这条路径把深度、候选着法范围、权重比例怎么取值的细节拆开讲。2. 五子棋棋盘表示与胜负判定课程里第一个可单独交付的模块2.1 15 路五子棋棋盘表示用嵌套列表而不是 numpy课程设计报告里的“数据表示”一章常见做法是 Python 3.9 加二维列表。15×15 的格子数只有 225 个用 numpy 提速的收益趋近于零反而引入两个新问题深拷贝时机不好把握答辩时还容易被追问“为什么用了 numpy 却没做矢量化”。个人倾向直接用嵌套 list外层 list 的每个元素是一行内层元素用 0、1、2 表示空、黑、白棋盘初始化一行就能写清楚。ROWS COLS 15 EMPTY, BLACK, WHITE 0, 1, 2 def init_board() - list: return [[EMPTY] * COLS for _ in range(ROWS)]参数说明ROWS和COLS固定 15 路对应五子棋最常见的比赛规则改成 19 路也能运行但后续搜索层的候选着法范围要同步压缩否则深度 4 的搜索会明显变卡。[[EMPTY] * COLS for _ in range(ROWS)]是每次迭代生成新行的写法不能偷懒写成[[EMPTY] * COLS] * ROWS后者会让所有内部行共享同一个引用改一个格子会把数据“摊派”到多行这是初版五子棋代码中最常见的隐性 bug没有之一。棋盘初始化保持无状态还有一层意义搜索函数需要对同一局面反复make_move / undo_move如果全局只此一份棋盘对象迭代加深和多线程调参都会发生状态污染。所以后续所有涉及棋盘改动的函数都应该把board作为参数传入、并保证在函数返回时恢复原状。2.2 五子棋胜负判定为什么不能只检查落子点的一个方向新手的“错误版本”通常是这样从最后一颗棋子出发沿四个方向分别向外扫描数到同色棋子大于等于 5 就判胜。这个写法在大部分单边连珠局面下成立但会漏掉落子点正好把连珠一分为二的情况例如棋盘上出现OOO X OOOX 为刚落的黑子如果只朝一个方向数只会得到 1但黑白双方实际已经连成七连。正确做法是朝正负两个方向同时延伸用累计总数判断是否达到五连。DIRECTIONS [(0, 1), (1, 0), (1, 1), (1, -1)] def count_line(board, row, col, dr, dc, player): total 1 for sign in (1, -1): r, c row dr * sign, col dc * sign while 0 r ROWS and 0 c COLS and board[r][c] player: total 1 r dr * sign c dc * sign return total def is_win(board, row, col, player): return any(count_line(board, row, col, dr, dc, player) 5 for dr, dc in DIRECTIONS)逻辑说明sign1和sign-1表示方向向量正反两向从落子点出发计数while循环的边界条件同时检查行、列是否越界因此棋子落在棋盘边缘也不会下标报错。any做短路求值四个方向一旦有一个达到五连就立刻返回 True。这里不需要检查“被对手夹断后再续连”的情况因为count_line只统计同色连续遇到空位或异色即停。2.3 边界用例与单测让胜负判定先于 GUI 被验证写完is_win后第一件事不是接 pygame而是做单元测试。为了在课程设计报告中能直接贴出测试结论我一般准备这样一组边界用例测试用例构造方式期望输出角落五连黑子连续排在棋盘左上角is_win返回 True对称长连落子前后各 3 个子返回 True一端被封堵四连一端为白子返回 False边缘断连五连跨过最后一行边界不抛异常且正确判断pytest test_wins.py -q参数说明-q只输出通过/失败统计方便报告中放截图。测试中不应有全局棋盘对象每个用例独立构造局部board否则 pygame 的刷新循环和测试线程会互相污染。这条测试策略也决定了后续搜索模块的形态凡是需要修改棋盘状态的函数都必须支持落子后恢复否则单测里会留下状态残留而这正是课程设计阶段排查难度最高的错误类型。3. 五子棋搜索算法层从极小极大到可解释的 α-β 剪枝3.1 为什么五子棋适合用极小极大分支因子是关键课程报告如果只写“我用了 α-β 剪枝搜索”略显单薄答辩被追问的最常见问题就是为什么不选蒙特卡洛树搜索或深度学习。五子棋 15 路棋盘的平均分支因子约 30 到 60深度 4 到 6 的 α-β 搜索完全能在秒级返回而且搜索路径可解释围棋的分支因子动辄 200 以上不借助策略网络和大量样本很难收敛。MCTS 也可以做但需要大量自对弈来保证整体评估稳定课程设计周期内想达到“知道该防守哪一手”的效果编写和调参成本反而更高。搜索深度理论节点数b4015 路棋盘 800ms 预算下的现实难度21600轻松完成42,560,000配合候选裁剪可接受6约 40 亿必须依赖强剪枝和着法排序表格的含义深度每增加 2节点数近似增加 b² 倍而实际可用时间只允许节点数在千万以内。这就是为什么“深度”和“候选着法范围”必须联合调整只调其中一个无法解决响应时间问题。3.2 α-β 剪枝三要素alpha、beta、候选着法裁剪α-β 剪枝不改变极小极大的最优决策只是砍掉那些父节点已经证明不可能被选中的子树。实现时有三个必备要素alpha 下界、beta 上界、以及候选着法裁剪范围。候选着法不需要遍历全部空位常见做法是收集“已有棋子周围两格以内”的空位这一手能把分支因子从 40 降到 20 上下比任何花哨剪枝都见效快。def get_candidate_moves(board, radius2): stones [(r, c) for r in range(ROWS) for c in range(COLS) if board[r][c] ! EMPTY] candidates set() for r, c in stones: for dr in range(-radius, radius 1): for dc in range(-radius, radius 1): nr, nc r dr, c dc if 0 nr ROWS and 0 nc COLS and board[nr][nc] EMPTY: candidates.add((nr, nc)) return list(candidates)参数说明radius2覆盖了活三、冲四成形时需要的落子范围radius1会让中盘进攻时漏掉关键点radius3则接近全盘搜索分支因子几乎不下降。集合candidates用于去重否则同一个空位会被多个既有棋子重复加入响应时间成倍上涨。3.3 α-β 搜索主循环落子、递归、回溯的顺序不能错def alphabeta(board, depth, alpha, beta, maximizing): moves get_candidate_moves(board) if depth 0 or not moves: return evaluate_board(board) if maximizing: value -float(inf) for r, c in moves: board[r][c] BLACK value max(value, alphabeta(board, depth - 1, alpha, beta, False)) board[r][c] EMPTY alpha max(alpha, value) if alpha beta: break return value else: value float(inf) for r, c in moves: board[r][c] WHITE value min(value, alphabeta(board, depth - 1, alpha, beta, True)) board[r][c] EMPTY beta min(beta, value) if alpha beta: break return value这里三个容易翻车的位置落子必须发生在递归调用之前、且递归返回后立刻恢复极大层和极小层各自负责更新alpha和beta不能混用候选列表必须去重否则同一个局面的评估值会被重复计算两次节点数表现为翻倍但不影响胜负。如果 AI 偶发地走出“帮对手连成五连”的坏手先查回溯是否被break跳过——break之前如果忘记board[r][c] EMPTY递归返回后会带着脏棋盘继续搜索。3.4 着法排序把高评估着法放到循环开头剪枝率翻倍α-β 的效率非常依赖探索顺序。先访问评估值最高的几个着法能更早触发alpha beta剪枝。常见做法是在get_candidate_moves之后对moves做一次轻量排序只统计候选点周围两格内的同色连子任何一个代价低于全盘evaluate_board的计数函数都可以用。def order_moves(board, moves, player): return sorted(moves, keylambda m: light_score(board, m[0], m[1], player), reverseTrue)参数逻辑light_score只计该点横、竖、两条斜线方向上各两格内的同色数返回一个 0 到 8 的整数排序后 α-β 的剪枝率通常能从 60% 出头提升到 80% 以上。这一步不改棋力但能把 800ms 时间预算内可搜到的深度提高一层是性价比最高的性能优化。4. 五子棋评估函数棋形分值表与调参策略4.1 只数连子数量不够棋形分类才是棋力来源评估函数是所有启发式搜索的灵魂也是最容易写“得过且过”的部分。最简单的版本确实是统计黑白两侧所有方向的连子总数然后相减——这样做的效果是 AI 能下出有威胁的棋但分不清“活三”和“被堵死的三连”于是常常全力进攻一条死路却漏防对方的活三。更合理的结构是按棋形分类赋分五连、活四、冲四、活三、冲三、活二每种棋形有独立权重。棋形必要特征参考分值FIVE五子以上连续1000000OPEN_FOUR四连且两端皆空100000CLOSED_FOUR冲四至少一端封堵8000OPEN_THREE活三两端可继续延伸5000CLOSED_THREE三连且一端封堵800OPEN_TWO活二300这张表的量级关系比绝对值重要FIVE 是终局值必须比其他棋形大得多CLOSED_FOUR 的权重必须高于 OPEN_THREE否则 AI 会看着对手的冲四成型而不去阻挡CLOSED_THREE 的权重要低于 OPEN_TWO因为被堵死的三连实际没有展开余地。答辩时主动解释这里用的是经验权重而不是学习权重反而能把话题引向自己的实验设计。4.2 评估代码容易被忽略的“开放端”判断评估函数中统计最频繁、也是 bug 最多的函数是“判断连子某端是否为空”。很多版本会把“两端皆空”和“一端为空”混为一谈这样冲四就会被当成活四AI 的防守策略会被完全带偏。正确实现需要沿着连子方向再向外探访一个格子。def in_board(r, c): return 0 r ROWS and 0 c COLS def is_open_end(board, r, c, dr, dc, length, player): front_empty in_board(r dr * length, c dc * length) and \ board[r dr * length][c dc * length] EMPTY back_empty in_board(r - dr, c - dc) and \ board[r - dr][c - dc] EMPTY return front_empty and back_empty参数说明这里的length是连子总长度dr、dc保持与count_line相同的方向约定r - dr表示连子起点的前一格。越界的格子一律视为非空否则board[-1]会引用最后一行造成完全错误的结果。我在实际实现里会把“两端皆空”和“至少一端为空”拆成两个 bool 返回因为合局判断中“冲四”只要求一端非空“活四”要求两端皆空合并写容易出纰漏。4.3 调优评估函数的 3 个关键参数与 5×5 网格搜索权重表是否合理的最终检验标准是对弈胜率而不是某一个局面的表现。课程设计阶段不必做深度强化学习只需固定搜索深度为 4用自对弈统计胜率。这里有三个必须调的参数参数建议初始搜索范围作用OPEN_FOUR / CLOSED_FOUR7:1 到 12:1控制攻击优先级OPEN_THREE / CLOSED_FOUR0.4 到 0.7防守灵敏度OPEN_TWO 绝对值200 到 400全局布局 vs 局部战斗grid [(9, 0.4, 240), (9, 0.5, 300), (7, 0.4, 240), (7, 0.5, 300), (5, 0.6, 360)] for ratio_four, ratio_three, open_two in grid: set_weights(OPEN_FOUR100000 * ratio_four, CLOSED_FOUR10000, OPEN_THREEint(10000 * ratio_three), OPEN_TWOopen_two) win_rate self_play(episodes20, max_depth4) print(ratio_four, ratio_three, open_two, win_rate)参数说明episodes20是权衡随机波动的常见规模20 局里最好固定先后手各 10 局五子棋先手优势非常明显否则统计结果会被先后手因素污染。max_depth4保证单局时间可控。网格里每一项跑完后别直接选胜率最高的组合还要观察败局集中在“漏防活三”还是“进攻不果断”哪一类。我常用的经验是漏防多就把ratio_three下调 10%进攻疲软就上调 10%一次只动一个维度方便对照胜负变化。5. 五子棋 AI 交付前的收敛验证时间预算、剪枝率与残局回归课程设计的最后阶段不是“跑通一次 GUI”而是让整个项目经得起“为什么这样设计”的追问。这里按三个方向做收敛检查。5.1 固定 800ms 时间预算用迭代加深兜住响应体验搜索深度固定为 4 时在低配机器上可能超时。常见做法是给搜索入口设一个时间预算每层递归开头检查时钟超时直接返回上一个完整深度的结果。这样即使候选着法数量突然增大也能保证每手棋稳定输出不会出现“长考”卡死。def timed_alphabeta(board, budget_ms800): return iterative_deepening(board, budget_msbudget_ms, max_depth6)预算值依据800ms 给 GUI 和事件循环留 200ms 余量避免棋子动画卡顿。报告里建议附一张“不同深度实际耗时”的表格比单纯写一句“深度 4 约 1 秒”更有说服力。5.2 剪枝率用数据证明 α-β 剪枝真实生效在alphabeta入口维护两个计数器已生成候选节点数和被剪枝节点数剪枝率等于被剪枝数除以生成数。这个数值放进报告既是性能论据也是排错线索低于 60% 通常是着法排序太差优先从靠近落子点的空位开始搜高于 95% 则可能说明评估函数区分度过高搜索在浅层就被固定到某一条主变容易漏掉迂回路线。5.3 残局回归集让每次调参都可回溯与其每次改完权重后手动下一整局不如维护一个轻量回归集。每行记录棋盘坐标、当前玩家、期望结果比如“三手内能否达成五连”并断言搜索着法与标注一致。改评估函数或候选半径后先跑回归集再自对弈能快速发现“原来会赢的残局现在不会下”的回归 bug。答辩时展示的最终成果应是一组断言通过的组合记录而不是一张无法证伪的截图。本文还有配套的精品资源点击获取
返回列表