ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

C++五子棋AI实现:极小化极大搜索与先手禁手规则

C++五子棋AI实现:极小化极大搜索与先手禁手规则 简介这是一份基于C实现的五子棋游戏完整源码涵盖人机对战与人人对战两种模式。电脑AI采用智能搜索算法并实现先手禁手规则包括三三、四四与长连判定适合希望深入理解博弈算法、棋盘逻辑与规则校验的C开发者学习。压缩包共22个文件以cpp源码、h头文件、obj中间文件及exe可执行程序为主整体仅681KB结构简洁便于直接编译调试。目前已有1335人学习下载。源码清晰拆分了主程序、棋盘表、AI模块与规则判断模块展示二维数组棋盘建模、合法落点检测、Minimax结合Alpha-Beta剪枝等关键实现同时附带可运行的exe可直观验证AI效果与禁手逻辑。配套文件中的pdb、ilk等调试信息也方便在Visual Studio环境中追踪问题有助于加深对编译链接和调试流程的认识。无论是学习C面向对象设计、GUI交互还是研究五子棋AI算法这份资源都能提供完整的工程级参考。1. 项目概述搞这个C五子棋我做了什么前阵子心血来潮用C手搓了一个带AI的五子棋小游戏源码里包含三件事一个能跑的完整对战程序、一个不靠随机下棋的人工智能、还有一套先手禁手的判定逻辑。这个项目的标题是“C五子棋源码有AI先手禁手”听起来不算复杂但真正动手之后你会发现把这三样东西揉在一起牵扯到的C知识点比你想象得多数组与内存布局、枚举与状态机、递归搜索、剪枝算法、评估函数的设计甚至还有一点规则建模的功夫。这个项目适合谁去研究第一类是正在学C的初学者想找个能串联起语法知识的练手项目——五子棋的棋盘天然适合用二维数组表达AI搜索能练到递归和深度优先遍历禁手判定则是对逻辑建模能力的完整考验。第二类是想做课程设计或者毕业设计的同学这个项目比单纯的控制台计算器有展示度比图书管理系统有技术含量AI对战本身就是个不错的演示亮点。第三类是单纯想搞懂“AI下棋到底是怎么想棋”的人避开复杂的机器学习用经典的极小化极大算法就能让程序具备还不错的棋力。先简单描述一下运行效果程序启动后进入15x15的棋盘界面玩家执黑先行AI执白后手。玩家落子后AI会在1到2秒内给出回应棋力虽然谈不上职业级但对普通玩家已经能造成足够的压迫感。更关键的是当玩家执黑时系统会在规则层面禁止三三、四四和长连这类先手必胜手段——所谓禁手就是国际标准五子棋连珠规则里为了平衡先后手差距而设的限制黑棋一旦走出禁手直接判负。这个规则的加入把一个普通的双人对战练习变成了一个带规则深度的策略游戏。2. 整体设计思路与方案选型2.1 为什么用控制台界面而不是图形界面很多新手拿到这个需求第一反应是上Qt、上EasyX、上SFML觉得没有图形界面就不叫游戏。但我的经验是如果核心目标是练C语言本身和算法设计控制台版本是性价比最高的选择。图形框架会引入大量与棋类逻辑无关的代码——坐标换算、事件循环、贴图资源、窗口刷新——这些内容会迅速淹没你要研究的核心算法调试复杂度也跟着翻倍。控制台版用标准库和几条控制台API就能实现15x15棋盘的渲染把时间省下来全部砸在AI和禁手逻辑上这才是这个项目的价值所在。而且棋盘渲染本身并不无聊我在渲染层里加了坐标轴标记、光标指示、胜负提示等细节观感上一点都不简陋。图形界面的问题在后面扩展时再解决不迟甚至可以用Web前端重新包装逻辑核心把C部分做成动态库调用这是后话。2.2 模块划分棋盘、规则、AI、UI四层分离我写代码有个习惯先想清楚分割线再动手。这个五子棋项目在结构上分成了四个核心模块彼此之间只暴露最小接口。棋盘存储模块负责所有落子状态的管理这是最基础的数据层用二维数组保存当前盘面提供落子、撤销、判断某位置是否为空等基础操作。规则模块包含两块一是胜负判定每次落子后检查是否有五连二是禁手判定专门针对黑棋检查是否形成三三、四四或长连。AI模块只做一件事给定盘面返回AI认为最优的落子位置。UI模块负责输入输出把棋盘的当前状态打印到屏幕上接收玩家的行列输入并驱动整局游戏的循环。这样的好处非常明显AI和规则是纯逻辑代码不依赖任何窗口和输入输出可以单独写单元测试。后期如果要换成图形界面只需要重写UI模块其余三个模块原封不动搬过去就行。模块之间的耦合只有“棋盘状态的读写”和“落子位置的传递”这两条线逻辑非常清晰。2.3 禁手规则对整体架构的影响禁手不是一个可以事后加进去的补丁它会影响数据层的设计。如果不做禁手一个棋盘类只需要两个功能能不能落子、落完有没有赢。但加了禁手后棋盘类必须额外提供“指定位置如果被黑棋落下是否会构成禁手”的查询接口这个接口要求棋盘能根据已有棋形快速统计三三、四四和长连的出现情况。所以我从第一步就把棋盘模块的接口设计成面向“问题查询”而不是面向“操作执行”。不仅是“在(x,y)放一枚棋子”更是“如果黑棋落在(x,y)当前盘面的规则状态是什么”。这样AI模块在评估候选点时才能同时考虑常规胜率和禁手风险。禁手规则还会改变AI的搜索逻辑——白棋获胜不仅可以通过自己连成五子还可以通过制造局面让黑棋被迫走出禁手这个高级战术我没有写进现在的评估函数里但架构上已经留好了扩展位置感兴趣的话可以在后面的扩展方向里看到思路。3. 核心实现与关键代码解读3.1 棋盘表示与胜负判定棋盘用的就是最朴素的15x15静态二维数组。棋盘类内部维护一个枚举数组每个位置取值为EMPTY、BLACK、WHITE三态之一。数组直接定义在栈上用固定大小而不是动态分配这样能避免内存管理的问题也方便用memset做快速重置。第一版胜负判定用的是最暴力的方式每次落子后从落子点出发向四个方向水平、垂直、两个对角线分别扫描统计同色棋子的连续数量如果达到5就判胜。比如水平方向先向左数到边界或异色棋停下再向右数总数加1就是水平和方向的连子数。这个方法的复杂度是O(1)因为每次最多只看四个方向每个方向最多检查10个格子而且不需要扫描整个棋盘来碰运气找胜利者——从落子点向外扩散是最符合直觉的路径。这里有一个新手容易踩的坑斜向扫描时坐标增量容易搞错。两个斜方向分别是(1,1)和(1,-1)后者在遍历时要注意纵坐标是否越界尤其当棋盘边界在左下角和右上角的时候一不小心就会数组越界。我封装了一个lambda表达式专门做单向扫描把“从某一点沿某方向数同色棋子”的逻辑收敛到一处四个方向循环调用即可既避免重复代码也降低出错概率。3.2 AI走棋的核心极小化极大搜索与评估函数AI是整个项目的灵魂。我的实现方案是经典的评价函数法配合限定深度的极小化极大搜索。核心思想很直白把棋局状态当做一个树形结构自己走一步后对方走一步如此交替展开在叶子节点用评估函数打分分数越高代表对黑方越有利假设AI执白则取负值后按黑方视角统一然后通过极小化极大回溯——自己会选分数最高的走法对手会选分数最低的走法——逐层回推根节点的最优分支就是AI当前的最优应对。搜索深度我设成了4层也就是AI想一步、对方想一步、再想一步、再回应一步。在纯C的递归实现下加上Alpha-Beta剪枝优化AI在普通桌面上能稳定跑到半秒到一秒之间返回结果。Alpha-Beta剪枝是极小化极大算法的标准优化在搜索过程中维护两个边界值alpha表示己方可接受的最高分数下限beta表示对手可接受的最低分数上限一旦某条分支的分数已经超出这个区间就果断剪掉剩余搜索不再浪费时间。评估函数的设计直接决定棋力的高低。我用了特征加权的方案扫描盘面上所有的五连窗口横、竖、两条斜线方向统计窗口内双方棋子的分布形态——活四、冲四、活三、眠三、活二等——每种形态赋予一个得分权重。活四给十万分冲四给一万分活三给五千分以此类推。然后对每个候选落子点计算“下在这里之后己方的增益分”减去“下在这里之后对方的增益分”两者相加就是该点的评估价值。这样AI天然会去抢占双方共同争夺的关键点表现出一定的攻防意识和压迫感。这里有一个我调试了很久才发现的问题AI在防守时不能只看自己受益的分数还要评估这个点对对方的增益。最开始我把评估值单纯定义为“己方增益”结果AI经常在对方有活三的时候不下在中间阻断点而是去边角发展自己的棋形棋力看上去非常弱。后来把公式改成“己方增益减去对方增益”问题立刻解决了。这个经验值得记下来任何棋类AI的评估函数都必须同时考虑攻防两端忽视防守的一方永远打不过懂得进攻的对手。3.3 禁手检测的实现细节禁手规则是黑棋独有的约束包括三类三三禁手同时形成两个以上活三、四四禁手同时形成两个以上冲四或活四、长连禁手连成六颗或六颗以上。白棋不受任何限制黑棋唯一合法的获胜方式是“五连”且仅“五连”。这个检测逻辑实现起来比想象中麻烦因为它不是一个简单的局部判断而是要对整个棋盘做模式扫描。第一版我用的是“逐点判断法”对黑方落子的每个位置先假定黑棋落在该点然后扫描经过该点的四条线上是否形成多个“活三”或“冲四”。这样做的计算量虽然大但逻辑意图清楚逐点判断时只需要处理落子点附近的变化不用全盘重新扫描。判断“活三”和“冲四”是个细活。活三的定义是存在一个三连的棋形左右两边都有空位且至少一端还能继续延展形成活四。冲四的定义是四连的棋形但只有一端是开放的下一步能形成五连。判断时我用了一个简单但有效的策略把落子点所在的每一条线上以落子点为中心向两边扩张提取出包含落子点在内的一段连续同色棋段记录两端的空位情况然后根据棋段长度和两端空位来分类。五连及以上直接判为长连禁手四连且两端都是空的算活四四连一端封闭一端开放算冲四三连两端都有空或者一端能延伸成活四的才算活三。最后统计这些分类的数量如果活三数量大于等于2或者冲四/活四数量大于等于2就判定为禁手位置。禁手检测的代码写得非常啰嗦但胜在逻辑直白。我宁愿多用几十行if判断也不愿意用过于精巧的数学归纳来压缩代码——因为这种逻辑一旦写错调试起来是灾难级的事。等所有规则都稳定之后如果你有兴趣再去考虑用位运算或者哈希表做性能优化也不迟前期还是以清晰为第一优先级。4. 实操过程与环境配置4.1 开发环境准备与编译运行这个项目不依赖任何第三方库标准C11以上就能编译通过所以环境的搭建非常简单。Windows上我用的是Visual Studio 2022新建一个控制台应用项目把源码文件丢进去直接编译即可。Linux或macOS上可以用g命令行编译只要支持C11标准就行。如果用的是VSCode需要自己配置好编译任务。我习惯的做法是创建一个tasks.json文件调用g编译整个项目下的所有cpp文件{ version: 2.0.0, tasks: [ { label: build gomoku, type: shell, command: g, args: [ -stdc11, -O2, -o, gomoku, main.cpp, Board.cpp, AI.cpp, Rules.cpp ], group: { kind: build, isDefault: true } } ] }这个配置里有几个地方需要注意-O2优化选项对于AI的递归搜索性能影响非常明显从调试用的-O0切换到-O2后同样的搜索深度耗时能减少一半以上编译时要确保所有cpp文件都出现在参数列表里漏掉任何一个文件都会导致链接失败生成的exe名称最好和项目名保持一致后面调试时不容易混淆。4.2 主循环与交互设计程序的主循环采用经典的“游戏状态机”模式。整个游戏运行在一个while循环里循环体内维护一个状态变量可能的取值包括PLAYER_TURN、AI_TURN、PLAYER_WIN、AI_WIN、DRAW。每次循环先根据当前状态渲染棋盘然后读取输入或调用AI走棋落子后立刻调用规则模块检查胜负和禁手最后更新状态并回到循环顶部。玩家输入部分我做了双重校验。第一步检查坐标格式是否正确接受类似“7,7”这样的行列输入用逗号分隔。第二步检查落子位置是否合法坐标是否在0到14的范围内、该位置是否已经有棋子。这两步校验缺一不可否则任何一个非法输入都可能让程序崩溃或者进入逻辑错误。为了让操作更顺手我还实现了简单的光标预览功能玩家可以用方向键移动光标按空格键落子这个交互在控制台上做起来并不复杂——每次重绘棋盘时在光标位置输出一个高亮的边款字符即可。AI走棋的过程是阻塞的。玩家落子后程序会打印一行“AI思考中”的提示然后调用AI模块的搜索函数。搜索期间界面会短暂卡住这是正常现象。如果你觉得等待时间太长可以调节搜索深度参数从4改成3会让响应时间缩短到0.1秒以内但棋力会明显下降。这个参数在代码里是个常量标注清楚方便调试时修改。4.3 测试与调试心得写完核心功能后测试环节花了我大量时间。最有效的测试方法是自动化脚本验证写一个测试程序自动模拟一系列预先设计好的棋局验证规则判定是否正确。比如构造一个黑棋在某个位置走出双三的局面检查禁手检测函数是否返回true再构造一个看似双三但其中一个三并不是活三的局面确认它不会误报。AI强度的调试则用“AI自对弈”来验证。我设计了一个自动模式让AI同时控制黑白双方一局结束后记录最终胜负。如果AI能多次执黑时走出禁手强制判负说明禁手检测有效如果执白时能抓住黑棋的禁手点取胜说明AI对禁手的利用逻辑是正确的。这种自动化测试比起手工一步一步下要高效得多推荐在调试时直接写进测试代码里。另外建议把日志功能打开。AI搜索过程中我把每个候选点的评估分数都打印到日志文件里这样能直观地看到AI的“思考过程”它认为哪一步价值最高、每个点位的攻防得分分别是多少。有了这层日志调试速度能提升一个量级因为你不必在黑盒状态下猜AI的行为逻辑了。5. 常见问题与排查技巧实录5.1 问题排查速查表问题现象可能原因解决思路编译时报“未定义的引用”链接阶段缺少某个cpp文件检查编译命令中是否列出了所有源文件棋盘显示的字符错位控制台字体宽度不一致切换到等宽字体或用两个半角字符模拟一个全角字符AI落子非常慢搜索深度过高或未启用优化降低深度改用-O2编译检查剪枝逻辑是否生效禁手误判频繁活三/冲四的判定条件过宽逐条打印检测过程中的棋段信息人工核对每条推断黑棋连成六子却判了胜长连禁手的检测被跳过确认胜负判定在禁手判定之前执行且黑棋先查禁手再判胜AI总在无意义的地方落子评估函数的攻防权重失衡检查“己方增益减对方增益”的公式是否存在防守缺失5.2 独家避坑心得第一个坑是胜负判定与禁手判定的执行顺序。我最初把“五连判胜”写在禁手判断之前结果黑棋走出长连时程序直接判定黑棋获胜完全没有触发禁手逻辑。正确的顺序是先判定黑棋落子是否构成禁手如果构成直接判黑负否则再检查是否五连判胜。这个执行顺序在规则上是有明确规定的不能简单靠代码的偶然顺序来决定。第二个坑是AI搜索中评估函数的对称性问题。棋盘上的棋形评估要确保方向一致性——水平方向的活三和垂直方向的活三有相同的价值权重。我第一版只扫描了水平方向导致AI在垂直方向的攻击力几乎为零。后来写成统一的方向数组把四个方向循环处理这个问题就迎刃而解了。第三个坑比较隐蔽Alpha-Beta剪枝里的极小极大值符号方向。搜索函数的参数中depth为偶数表示己方走棋取极大奇数表示对方走棋取极小。如果有人把这个符号写反了AI就会变得异常古怪——有时会故意送对方活三有时又会走出毫无威胁的废棋。调试时我先在评估函数里加入一个临时打印把每个分支的评价值输出对照手算结果检查极小极大回溯的方向是否一致很快就找到了问题所在。第四个坑是控制台输入的缓冲问题。使用标准库的cin读取行列数字时如果玩家输入了非数字字符比如字母“q”想要退出cin会进入错误状态导致后续所有输入全部失效。我在读取输入后加了fail状态的判断和清除缓冲区的处理这样即使输入错误程序也能继续正常运行而不是直接崩溃。6. 后续可以继续拓展的方向这个项目做完后我从它身上获得的收获比预期大得多。它不仅让我重新捋了一遍C的基础语法还让我对递归、状态搜索、特征加权这些算法概念有了非常具象的认知。如果你也想复制这个项目我的建议是不要只是把源码抄一遍而是先自己画出模块图和流程图再对照实现这样学到的才是自己的东西。如果你做完基础版之后还想继续深入我提供几个实际的扩展方向作为参考这个版本你的基础逻辑不动只增加冲突检测判断即可无缝支持。社区里关于这个模式的讨论不算多但做出来后的成就感极高。整个调试过程中我个人最深的一点体会是棋类AI项目对代码质量的要求比普通业务代码高得多。因为每一步棋背后都嵌套着多层递归和复杂的规则判定任何一层的微小疏漏都会让最终结果表现得异常诡异。但这种项目恰恰是训练C功底的最好试金石——它逼着你去思考什么是清晰的数据结构、什么是可靠的模块接口、什么是可验证的逻辑分支。把这些基本功练扎实了后面再去接触更复杂的系统开发底气会完全不一样。本文还有配套的精品资源点击获取
返回列表