简介这份C语言实现的五子棋AI支持连珠规则面向对博弈树搜索、启发式评估及效率优化感兴趣的开发者与算法学习者。代码基于经典的极小极大搜索框架集成了阿尔法贝塔剪枝、迭代加深、主变例搜索、杀手启发、连续冲四与连续活三搜索等典型增强手段还引入预评估和动态多线程调度兼顾搜索速度与稳定性是一份结构清晰、内容完整的对抗博弈程序参考实现。资源共3个文件全部为C语言源码压缩包仅11KB包含Linux版、Windows版以及竞赛专用版本便于读者在跨平台移植和比赛场景中对照分析。已有457人学习下载。通过阅读这套源码可深入理解主流搜索算法如何实际落地理清搜索深度、剪枝效率与走法排序之间的关系同时参考其预评估和杀棋搜索的组织方式无论用于课程设计、算法竞赛还是作为后续二次开发的基础都有直接帮助。 前阵子刷到“开宝五子棋陪练”这类应用时不少棋友私信问我五子棋AI到底是怎么写出来的。我正好有一个自己手写的C语言Renju规则五子棋AI棋盘15路支持黑棋禁手判断核心就是“极小极大搜索 启发式评估”这条经典路线。上周末把它重新翻出来删掉了一些临时补丁顺便把整个项目的拆解过程完整记录下来。这篇东西适合刚学完C语言基础、想找一个有硬壳项目练手的人也适合对棋盘博弈AI怎么落地感兴趣的人参考——我只讲自己实际敲过的方案不灌水。1. 项目定位C语言做五子棋AI的动机与规则取舍1.1 为什么是C语言来写AI引擎市面上用Python、JavaScript写五子棋AI的教程一抓一大把但真正要跑Renju规则的AI对性能还是有要求的。我的第一版用Python写过棋盘评估函数一跑下一层搜索就要等两三秒完全不像下棋。换成C语言之后同样深度的搜索时间基本可以压缩到毫秒级原因是C没有运行时解释开销内存布局也完全可以自己控制。另一个原因是控制力。C语言里棋盘、方向数组、递归搜索都可以用裸数组和指针搞定反而更容易理解博弈AI的运行过程。你不需要依赖框架偷偷帮你做了多少工作每一步逻辑都明明白白写在代码里。对学习目的来说这种“笨”恰恰是价值。很多生产环境里的棋类AI引擎比如一些较老的连珠引擎核心部分也还是C/C。如果你以后想研究Rapfi这类重量级五子棋AI的源码C语言的底子几乎是必须的。这个项目的定位不是做出一个能赢职业棋手的程序而是用最少的依赖把Renju规则下AI决策的完整链路跑通。1.2 Renju规则禁手到底改变了什么普通五子棋里双方只要先连成五就赢策略相对简单。Renju连珠规则最大的不同是黑棋先手方被加了三条禁手不能下出“三三”、“四四”和“长连”。三三禁手黑棋落下一步棋后同时形成两个或以上的活三。四四禁手黑棋落下一步棋后同时形成两个或以上的冲四包括活四。长连禁手黑棋落下一步棋后横竖或斜向出现连续6颗及以上黑子。为什么会有这种规则因为无禁手五子棋黑棋先手优势太大职业层面黑棋几乎必胜。加上禁手后黑棋进攻时要时刻留意底线白棋则可以利用黑棋的禁手点进行反制。对AI来说这不只是多几个if判断而是评估函数和搜索逻辑都要跟着变。我见过不少用C语言写五子棋的人一开始只写了无禁手版本后来想升级Renju结果在禁手判断上被卡了两三周。核心原因是没有把“落子后的棋型变化”想清楚。如果你只做无禁手AI禁手模块完全可以不写但要做Renju规则禁手判断不是附加功能而是AI会不会“送死”的关键。2. 核心引擎棋型扫描与评估函数2.1 棋盘结构与落子基础我用最直接的方式全局二维数组int board[15][15]0表示空1表示黑棋2表示白棋。棋盘外再留一圈边界用3表示“墙壁”扫描时统一判断省去很多边界if。#define SIZE 15 #define EMPTY 0 #define BLACK 1 #define WHITE 2 #define WALL 3 int board[SIZE 2][SIZE 2]; void init_board(void) { for (int i 0; i SIZE 2; i) for (int j 0; j SIZE 2; j) board[i][j] (i 0 || i SIZE 1 || j 0 || j SIZE 1) ? WALL : EMPTY; }四个方向向量dirs[4][2] {{0,1},{1,0},{1,1},{1,-1}}就够用了因为五子棋在一条直线上不区分正反方向扫描时一次向前一次向后合并统计。落子时要做合法性检查坐标必须在1到15之间、位置为空、且如果落黑子还要检查不是禁手点。很多新手会把“检查禁手”直接做成“黑棋不允许下”这不对。禁手点对黑棋是非法但白棋可以故意下在那里而且黑棋禁手不是“不能形成三三”而是“落子后不能形成两个活三”。所以检查的时机必须在落子后的临时状态下。2.2 一条线上怎么数棋型评估函数主要看“某个点在四个方向上形成的棋型”。常见做法是枚举所有包含当前点的可行五元组窗口统计空位和双方棋子数。这种实现简单但重复计数很严重。我后来改成方向合并扫描法才稳定下来。核心思路是从落子点出发沿某一方向向两端延伸数出连续同色棋子的长度再看两端状态。伪代码大概是typedef struct { int len; // 连续同色棋子长度 int block; // 两端被封堵数量0/1/2 int open_end; // 两端都是空位的数量 } LineInfo; LineInfo scan_line(int x, int y, int color, int dx, int dy) { LineInfo info {1, 0, 0}; // 正向延伸 for (int step 1; ; step) { int nx x dx * step, ny y dy * step; if (board[nx][ny] color) info.len; else { if (board[nx][ny] EMPTY) info.open_end; else info.block; break; } } // 反向延伸逻辑相同 // ... return info; }拿到LineInfo后结合长度和两端状态就能分档长度≥5且是黑棋时先判断长连。长度4且两端都空活四。长度4且只有一个空端冲四。长度3且两端都空活三。长度3且有一个空端眠三。长度2且两端都空活二。不过这种简化版会把一部分“跳活三”漏掉。比如黑白间隔形成的“眠三”实际上是活三的情况。更严谨的Renju裁判程序会用模式匹配而不是简单看连续长度。我的处理是接受这个简化棋力大约有职业初段以下但作为学习项目完全够用。如果你想接近比赛级需要继续延伸判断我后面会说。2.3 评估权重表与黑白分差AI要决策就得把局面变成数值。我的评估分两部分先给当前落点打一个局部启发分数再进行全盘评估。局部启发分数用于候选点排序遍历四个方向统计以该点为中心、半径2范围内双方棋型。权重表大致是棋型权重成五1000000活四100000冲四10000活三5000眠三500活二200眠二20单子5这些数不是拍脑袋定的核心原则是“成五必须远大于活四活四必须远大于任何三”。如果活四只比冲四高一点点AI可能会在该连五的时候去堵一个不关键的眠三。我第一版权重差距不够AI经常在快赢时跑去防守后来把成五和活四的差距拉到10倍以上才正常。全盘评估时我分别计算黑棋总分和白棋总分返回值是黑棋总分 - 白棋总分。搜索时轮到哪一方就走哪一方的目标极大极小决策用黑白分差作为统一指标。这样比“只算当前玩家收益”稳定得多。需要提醒的是评估黑棋时不能机械地把所有棋型都加进去因为黑棋存在禁手某些看似高分的位置实际上不能下搜索层必须提前过滤。3. Renju禁手判断最容易翻车的地方3.1 三三、四四、长连的准确语义禁手判断要考虑的是“黑棋落子后新产生的棋型”而不是扫描整盘棋里的历史棋型。比如说黑棋已经有三个活三现在下这步棋没有新增活三那么这步不算三三禁手因为禁手指的是落子动作造成的局面。三三禁手里的“活三”不是普通的活三而是“能发展成活四或冲四”的三。以15路棋盘的标准连珠规则活三包括两头无挡的连续三子和一部分跳三。最准确的理解落子后如果这个三所在的直线在下一步存在至少一个点可以让黑棋形成活四或冲四才算是有效活三。同理四四禁手里的“四”指冲四或活四。两个方向各形成一个冲四也是禁手一个活四加一个冲四同样禁手。长连则是落子后出现6颗以上连续黑子。这里有个容易判断错误的点如果黑棋落子后既形成五连同时形成两个三不算禁手因为“先连成五”优先胜负已经产生。但Renju规则下黑棋“长连不算五连赢”长连是输棋这一点和五连有本质区别。3.2 方向合并法实现禁手判断判断禁手时我会在当前局面直接“试放”一枚黑棋然后检查四个方向。int is_forbidden(int x, int y) { int test_board[SIZE 2][SIZE 2]; memcpy(test_board, board, sizeof(board)); test_board[x][y] BLACK; int live_three 0, four_count 0; for (int d 0; d 4; d) { LineInfo info scan_line_on_board(test_board, x, y, BLACK, dirs[d]); if (info.len 6) return 1; // 长连 int type classify_line(info); if (type LIVE_THREE) live_three; else if (type FOUR) four_count; } if (live_three 2) return 1; if (four_count 2) return 1; return 0; }注意方向合并的关键点水平方向扫描一次垂直方向、两个斜方向各扫描一次总共4个LineInfo。如果在水平方向既有活三又有冲四不可能因为连续棋子来自同一条直线分类结果只有一个。四方向合并才能代表真正同时产生的多个棋型。classify_line里要小心长度4且两端都空是活四但活四在禁手判定中也算一个“四”。长度4一端空一端被挡算冲四。长度3两端都空算活三但如果长度3一端空另一端空位再往外一格还是空这叫“跳活三”或“眠三变活三”我建议先不把它当活三避免误报。我自己踩过最大的坑是“重复统计”。用五元组窗口法时一个活三可能在三个窗口里被重复统计结果普通黑棋落子后误判成三三。后来改成方向合并后故障就消失了。所以如果禁手实测经常误报优先检查统计单位是否统一。3.3 AI决策时怎么用禁手信息禁手模块在AI里有两个用途。第一个是生成合法招法AI如果执黑棋搜索前要过滤所有禁手点AI如果执白棋则要主动利用黑棋禁手。我的做法是AI默认执白玩家执黑白棋没有禁手限制黑棋玩家要手动遵守禁手规则AI则在评估时把“黑棋下一步禁手点”作为白棋的机会。具体策略是白棋的候选点生成时额外检查“如果黑棋下这个点是否会禁手”。如果一个点黑棋落下去是禁手那么该点对白棋来说就是天然的防守点甚至可以作为进攻陷阱。比如我可以故意给黑棋留一个三三禁手点黑棋如果不查规则下了就直接判输。AI执黑时搜索节点的合法招法必须调用is_forbidden过滤。这会增加不少计算量但在候选点已经按启发式排序后实际影响可控。过滤后的结果必须缓存下来避免同一个点在多次评估时反复调用禁手函数。4. AI搜索模块让AI不只盯眼前4.1 极大极小搜索与alpha-beta剪枝评估函数只负责打分AI的“远见”来自搜索。我用的是经典的负极大值搜索框架本质上就是极大极小的变体。int negamax(int depth, int alpha, int beta, int color) { if (depth 0) return evaluate_board(); Move moves[MAX_MOVES]; int move_count generate_moves(moves); for (int i 0; i move_count; i) { if (is_forbidden(moves[i].x, moves[i].y, color)) continue; apply_move(moves[i]); int score -negamax(depth - 1, -beta, -alpha, opponent(color)); undo_move(); if (score alpha) { alpha score; } if (alpha beta) break; // 剪枝 } return alpha; }负极大值的好处是代码简洁颜色切换时直接取负号避免分玩家写两套逻辑。剪枝条件alpha beta是核心它能砍掉大量无效分支。如果完全不剪枝深度4的时候节点数量可能到几百万剪枝后平均能少一个数量级以上。但注意剪枝效率完全取决于招法顺序。如果AI先搜索垃圾点alpha长期不更新剪枝就剪不掉多少。所以候选点的排序比搜索本身更影响性能。4.2 候选落子点生成与排序五子棋不像围棋不能下在无关位置。我的generate_moves只收集“距离任意已有棋子不超过2格”的空位。这个启发式假设很实用离棋子太远的位置在多数情况下无关紧要。收集到候选点后用2.3节的局部启发分数快速排序。我只算以下四项分数若落白子能形成多少进攻棋型若落白子能破坏多少潜在黑棋棋型若落黑子能形成多少黑棋棋型若落黑子是否禁手。然后按照“防守分 进攻分”混合排序。实操下来排序质量比预想中重要太多。我试过完全不排序深度4搜索一局要几十秒按启发分排序后同样的深度一两秒内完成棋力还提升了因为剪枝更准确。候选点数量也有限制。每层最多取前12个落点配合深度4搜索已经能战胜大部分业余棋手。如果放开到所有候选点性能会急剧恶化但棋力提升并不明显。这个限制也是很多商业小型AI的常见做法。4.3 搜索深度、时间预算与棋力平衡深度选择是AI调参的核心。我试过深度2、深度4和深度6深度2反应快但基本是“近视眼”只能做局部攻防。深度4速度和棋力平衡点普通笔记本上每次落子在几百毫秒到2秒之间。深度6开局残局可接受但中盘随机性大偶尔会超时不适合作默认设置。更好的办法是引入时间预算。每次AI落子前设定一个时间上限比如1.5秒迭代加深搜索先深度2再深度3、4……超时就用上一次完整深度的结果。很多成熟引擎都采用这种思路游戏体验比固定深度好得多。迭代加深配合置换表可以进一步提速但在C语言项目里实现置换表要用哈希表管理棋盘状态代码量会翻倍。我给新手的建议是先把深度4和合理剪枝做到位再考虑置换表。5. 调试实录与常见坑位速查5.1 AI总是在不该松的地方松手我见过最典型的症状是AI明明已经可以连五了却下到一边去堵对方一个不太重要的二。排查到最后问题在评估函数权重设置不合理。我最初的成五权重是10000活四是8000看起来差不多但因为搜索深度不够AI“看不到”两步后的成五而活四当前得分更高于是就去走活四了。解决方法是把“一步成五”的权重拉到极高的优先级同时确保在生成候选点时“能直接连五”的落点排在第一位。更保险的做法是加一个快速检测如果当前局面存在直接成五的点AI直接返回该点不进入搜索。这一步是棋力下限的保证。5.2 禁手误报错报排查路径禁手逻辑出问题时先不要整体检查AI搜索而是写一个专门的小工具遍历所有黑棋可落点输出每个点的LineInfo和三三数量。我当时就是靠这个工具发现了“跳活三漏判”和“五元组重复计数”两个问题。另外一个心理误区是很多人以为禁手是“当前局面是否已有两个活三”其实不是。禁手是“这步棋落下去后是否形成两个活三”。必须临时模拟落子再扫描。我用memcpy临时拷贝棋盘虽然多花一点内存但逻辑非常清晰调试时不容易出错。需要优化时再改成现场落子撤销。5.3 性能优化技巧与问题速查表性能主要花在评估函数和候选点生成。我的优化顺序是先做招法排序让alpha-beta剪枝生效再限制每层候选点数量最后优化评估函数的扫描函数减少重复访问内存。我曾经试过把棋盘数据改成位棋盘用位运算扫描棋型理论上更快但代码复杂度增长明显对学习项目性价比不高。如果你的AI要跑到深度6以上再考虑位棋盘。下面是我调试过程中的一个速查表很多问题照着排查就能定位问题现象可能原因排查方向AI看不到下一步成五评估权重不够悬殊直接成五权重提到百万级并加快速检测同层搜索极慢招法排序差检查候选点排序提高防守分权重黑棋莫名被判禁手重复统计棋型改用方向合并法确保一条直线只算一次黑棋应该禁手却显示合法只检查静态局面临时模拟落子后再扫描AI总在无谓防守搜索深度太低或防守权重过高调低防守权重增加迭代加深开局总是固定套路没有随机化或开局库在权重相近时加入随机扰动Ranju规则五子棋AI最难的不是搜索而是评估和禁手。等你把这些模块都跑通后会发现自己对C语言内存、递归和算法剪枝的理解都提升了一个档次。我后来把核心引擎接到了命令行界面和简单图形界面上整个引擎代码也就两千行左右但已经能稳定和普通玩家切磋。如果你也想动手写一个建议先从无禁手AI开始跑通搜索后再逐步加入禁手逻辑这样调试思路会清晰很多。本文还有配套的精品资源点击获取