ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C++五子棋AI源码解析:极大极小值算法与AlphaBeta剪枝实战

C++五子棋AI源码解析:极大极小值算法与AlphaBeta剪枝实战 简介C实现的五子棋游戏源码核心采用极大极小值算法与AlphaBeta剪枝传统搜索算法前后端完整可运行。资源面向计算机相关专业学生适合作为毕业设计、课程设计或期末大作业也适合希望学习经典博弈搜索算法并练习项目实战的开发者。压缩包共66个文件体积仅1.35MB涵盖C头文件与源文件、JSON配置、HTML/CSS/JS前端页面、Markdown项目文档以及PNG/GIF演示录屏目录结构清晰。代码按游戏控制、棋盘、AI、服务端、视图等模块拆分并包含WebSocket通信和CMake构建配置便于直接阅读、调试和二次开发能够直观理解AlphaBeta剪枝在实际五子棋对战中的应用与工程落地方式。目前已有359人学习下载若希望以完整项目为蓝本完成课设或毕设并深入掌握经典搜索算法的代码实现这份资源提供了很好的参照。1. 这套五子棋源码值不值得研究搜索算法才是技术分水岭你从网上下过 C 五子棋小游戏源码就会发现能画出棋盘、能下满一盘的控制台程序一抓一大把但真正敢写“极大极小值算法 AlphaBeta剪枝”的源码少得多。绝大多数 C 小游戏教程的 AI 只会扫描当前棋盘找一个“看起来能连三连四”的空位那种程序跟人下很容易被堵死。而这个标题里的五子棋游戏源码核心价值不在前端界面而在搜索算法它让 AI 像人一样提前算好几步再决定往哪走。我接下来按这套源码最常见的结构拆给你看从评估函数到前后端接口再到实际调参时的血泪经验。适合已经有点 C 基础、想拿博弈搜索练手或者正在做课程设计/实训项目的人。2. 极大极小值算法和AlphaBeta剪枝评估函数、搜索树与剪枝条件网上搜“alphabeta剪枝算法专题”能看到一堆公式和树形图但看完还是写不出五子棋 AI原因很统一绝大多数人把剪枝当主角却忘了它服务的对象是极大极小值算法而极大极小值算法又依赖评估函数。我一般会按“评估函数 → 搜索树 → 剪枝条件”这个顺序讲因为代码写崩十次有八次是评估函数没立住。2.1 评估函数怎么量化棋局先解决“怎么赢”五子棋搜索的输入是棋盘输出是分数这个分数必须能回答一个问题当前局面到底对谁有利有利多少。你不需要引入机器学习传统做法是给棋型打分然后遍历棋盘的所有行、列、斜线累加双方棋型分数最后让“我方总得分减去对方总得分”作为局面评价值。为什么用差值因为五子棋是零和游戏我用差值就等于同时考虑了进攻和防守。先给一套我在小项目里常用的棋型分值表你打开源码后看到的评估函数大概率也是这个思路棋型分值说明连五100000已经获胜必须给到极大值活四50000对方怎么堵都挡不住两手内必胜冲四5000只有一个方向能形成五连有威胁但能堵活三2000再走一步变活四属于核心进攻棋型眠三300受己方或边界限制威胁小但可能组合出杀活二200远期的进攻潜力眠二20几乎可以忽略但比没有好这套分值不是玄学它要满足一个基本约束两只活三组合起来的威胁应该大于一个冲四或接近冲四否则 AI 会去抓那些看起来分高但实际上没接续的棋。你拿到源码后第一步就该先看评估函数如果分值表里冲四低于活三或者连五没有设成最高那搜索出来的着法一定有问题这种惯性排序错误是 C 五子棋源码最普遍的翻车点。评估函数还要区分“死活”。一个棋型被己方、对方或边界堵住后价值会断崖式下降。常见实现是对每条横/竖/斜线做模式匹配找出连续同色棋子以及两端的状态两端都空是活棋型一端空是冲或眠棋型两端全死就不要记分了。这个过程可以单独拆一个evaluate_line函数方便后面做单元测试。2.2 极大极小值算法的搜索树与胜负传递逻辑有了评估函数搜索树才能工作。极大极小值算法的思路很直白轮到 AI 走棋AI 想找分数最高的分支轮到对手走棋对手会找让 AI 分数最低的分支。搜索深度加深时这组“最大最小”会交替进行直到到达叶子节点再用评估函数打出这一层的分数。用 C 写博奕搜索时纯极大极小写出来很啰嗦每层要判断当前是 Max 节点还是 Min 节点。更常见的是写成“负极大值”不管当前轮到谁都从当前玩家的视角评估局面然后递归返回负值。假设当前玩家得到分数为score那么对手在同一个局面的视角就是-score。这样极大极小就压缩成了一个递归函数逻辑上也好理解。我见过不少初学 C 的人把这一步实现反了递归调用时没有对返回值取负或者递归返回后没有更新 alpha。最后表现就是 AI 每步都走得很怪甚至能走出连自己的活四都不管的棋。这种问题极难用肉眼看出来因为它在某些局面下又是正常的。所以在你写代码之前先把这个规则记下来每个递归层自己执行落子调用下一层时必须把传入的 alpha、beta 取负再交换位置下一层返回值也要取负后才是当前层的真实分数。这个口诀后面还会反复用到。2.3 AlphaBeta剪枝把搜索层数翻倍的三个条件极大极小值算法有个致命问题十五路棋盘每层可能有几十个候选点搜到第 4 层就要上百万节点深度再往上就扛不住。AlphaBeta 剪枝做的事情是在搜索过程中发现某个分支“无论如何都不会比已经搜到的结果更好”就直接跳过不做无用计算。剪枝能生效要满足三个条件。第一搜索顺序必须好。AlphaBeta 的效率高度依赖先搜哪个分支如果每次都先搜到“最佳着法”alpha 和 beta 的边界会收得非常快后续分支很容易被剪掉如果每次都先搜烂棋剪枝率低得可怜退化成完整极大极小。第二alpha 和 beta 初始区间要正确。常见做法是alpha -INFbeta INF搜索过程中不断收紧。第三递归实现里更新 alpha 后要立刻判断if (alpha beta) break这个判断必须在每层循环内尽早执行漏掉就没剪枝了。我不会把剪枝理解成“优化技巧”它更像是这套源码能不能跑起来的前提。同一台机器同样搜深度 6有剪枝和没剪枝的耗时能差几十倍。很多 C 五子棋源码打开就看到minmax_search函数里只有递归没有 alpha/beta 参数那基本就是标题党跟这个标题里的技术点不是一回事。3. C 落地实现棋盘、着法生成、递归搜索的最小可运行版本这一章进入正题。拿到任何源码包我习惯先找三个东西棋盘的表示方式、着法生成函数、递归搜索函数。这三块确认了整个引擎的骨架就清楚了。下面按我经常组织的工程结构把每一步代码写给你。3.1 棋盘的数据结构二维数组、位棋盘和边界判断五子棋棋盘标准是 15×15一盘棋最多 225 个交叉点用二维数组完全够。有的源码为了性能会搞位棋盘把棋盘压成几个 uint64_t这对五子棋来说收益不大还会让代码变得难以调试我一般不用除非你要参加计算性能比赛。#include vector // 0 表示空1 表示黑棋2 表示白棋 using Board std::vectorstd::vectorint; Board create_board(int size 15) { return Board(size, std::vectorint(size, 0)); }逻辑说明用vectorvectorint的好处是访问board[x][y]和直觉一致而且std::vector自带内存管理方便在各种 C 标准下编译。参数size默认 15也允许你改成 9 或 19这在调试评估函数时很有用小棋盘能更快跑完递归。如果你在 Dev C 上编译留意老版本对vector嵌套的报错本质上不是代码错了而是编译器标准没切到 C11 以上。3.2 着法生成邻域扩张与排序这是剪枝效率的分水岭搜索每层都会问下一步可以下在哪朴素做法是遍历整个棋盘找所有空位15×15 棋盘每层最多有 200 多个候选点搜索深度一上来分支直接爆炸。五子棋有个经验规则有效落子只可能出现在已有棋子周围两格范围内离所有棋子太远的点在博弈上几乎毫无意义。#include vector #include utility #include set void generate_moves(const Board board, int last_x, int last_y, int range, std::vectorstd::pairint,int result) { std::setstd::pairint,int seen; int n (int)board.size(); for (int dx -range; dx range; dx) { for (int dy -range; dy range; dy) { int nx last_x dx; int ny last_y dy; if (nx 0 || nx n || ny 0 || ny n) continue; if (board[nx][ny] ! 0) continue; if (seen.insert({nx, ny}).second) { result.push_back({nx, ny}); } } } }逻辑说明这个函数从最近的一手棋开始把它周围range格范围内的空位全部收进候选列表并用set去重。range取 1 时每层候选点大约 10~20 个取 2 时会增加到 40 个左右我通常取 2因为五子棋的活三、冲四经常需要隔一个空位才能形成后续杀招范围太小会漏棋。实际源码里你不会每次从零收集而是维护一个“已占点集合”或候选点列表每落一子就增量更新这是性能优化的一部分。但候选点多不等于搜索就慢真正要命的是顺序。AlphaBeta 剪枝对顺序极其敏感如果先把最好的着法放前面剪枝率可能到 90% 以上如果按坐标从小到大搜搜索树会膨胀得非常厉害。所以你还需要一步排序#include algorithm void sort_moves(Board board, int current_player, std::vectorstd::pairint,int moves) { // 简单启发优先搜索能形成连五、堵对方活四或形成活四的位置 std::sort(moves.begin(), moves.end(), [](const auto a, const auto b) { int score_a quick_score(board, current_player, a.first, a.second); int score_b quick_score(board, current_player, b.first, b.second); return score_a score_b; }); }逻辑说明quick_score不需要真去跑完整评估它只检查落子后是否形成连五、活四或堵住对方的冲四活三给这些动作加一个高权重。这个函数开销越小越好因为每次递归都要调用如果它本身写得太重那排序带来的收益会被排序成本抵消。很多源码不写排序这一步AlphaBeta 剪枝等于白开这也是我拿到源码后一定会先看有没有排序函数的原因。3.3 递归搜索函数极大极小与剪枝的核心 C 代码下面这段是你搜索算法的“心脏”也是整个源码里最值得反复读的部分。我写成负极大值形式把极大极小和剪枝放在同一个递归里。#include climits const int INF 1000000000; int evaluate(const Board board, int player); int check_winner(const Board board); int negamax_search(Board board, int depth, int alpha, int beta, int player) { // 1. 检查终局当前视角下赢返回极大值输返回极小值 int winner check_winner(board); if (winner player) return INF; if (winner ! 0) return -INF; // 2. 深度耗尽用评估函数给局面打分 if (depth 0) { return evaluate(board, player); } // 3. 生成候选点并排序 std::vectorstd::pairint,int moves; generate_moves_from_all(board, 2, moves); sort_moves(board, player, moves); // 4. 遍历候选点递归搜索并剪枝 for (const auto move : moves) { board[move.first][move.second] player; int score -negamax_search(board, depth - 1, -beta, -alpha, 3 - player); board[move.first][move.second] 0; if (score alpha) { alpha score; } if (alpha beta) { break; // AlphaBeta 剪枝 } } return alpha; }逻辑说明核心在递归调用那一行。当前玩家落子后棋盘视角切到对手所以递归调用里player变成3 - player同时 beta 和 alpha 要取负再交换位置因为负极大值要求“对手的最优 当前玩家视角的最差”这正好让那个差值评估函数在双方视角下保持一致。如果漏掉-评分会被错误地放大或缩小整棵树就废了。参数说明depth是剩余搜索深度alpha是当前玩家能保证的下界beta是上界player是当前行动方。INF设成 1e9 是为了给胜负结果一个足够大的值让任何普通棋型分值的累加都追不上它。剪枝条件alpha beta不能写成因为等于时也说明后续分支已无意义少剪一个分支就少一份性能。我见过有人把这里的改成结果节点数翻倍搜索超时所以这个细节你也检查一下。3.4 迭代加深与时间控制别让 UI 卡死固定深度搜索有一个问题你不知道这一层要跑多久。可能深度 5 很快深度 6 突然卡住几十秒。实战五子棋 AI 更常见的做法是迭代加深从深度 1 开始逐层往上搜每层结果都保存起来如果时间到了就把上一层结果拿出来用。#include chrono using Clock std::chrono::steady_clock; bool time_up(const Clock::time_point start, int time_limit_ms) { auto now Clock::now(); auto elapsed std::chrono::duration_caststd::chrono::milliseconds(now - start); return elapsed.count() time_limit_ms; } std::pairint,int iterative_search(Board board, int max_depth, int time_limit_ms) { auto start Clock::now(); std::pairint,int best_move {-1, -1}; int alpha -INF; int beta INF; for (int depth 1; depth max_depth; depth) { std::vectorstd::pairint,int moves; generate_moves_from_all(board, 2, moves); sort_moves(board, 1, moves); for (const auto move : moves) { if (time_up(start, time_limit_ms)) { // 时间到直接返回上一层已经搜到的结果 return best_move; } board[move.first][move.second] 1; int score -negamax_search(board, depth - 1, -beta, -alpha, 2); board[move.first][move.second] 0; if (score alpha) { alpha score; best_move move; } } // 进入下一层前把窗口重新放宽避免上层的 alpha 限制过死 alpha -INF; beta INF; } return best_move; }逻辑说明迭代加深会让总耗时略高于单次固定深度搜索但它换来了“随时可以返回一个可用的走法”这在前后端分离的架构里几乎是必须的。注意每一层结束后我把 alpha/beta 重新初始化不然上一层搜索的边界值会污染下一层的剪枝判断导致丢深度。时间检查放在走法循环里而不是递归函数内部这样开销最小。如果你拿到源码里既有negamax_search又有iterative_search说明作者已经把实战问题考虑进去。如果只有固定深度多半是学习演示版你要自己补上这一层时间保护。4. 前端后端怎么拆C 搜索引擎与界面之间的接口约定标题里的“含前端后端”放到 C 程序里指的不是网络服务而是逻辑分层后端是搜索引擎前端是棋盘绘制和人机交互。无论你打开这个 zip 看到的是控制台界面、Qt 界面还是 Web 前端分层思路都差不多。关键在于别把搜索代码和界面代码写成一个难以拆分的 main.cpp。4.1 为什么后端不能耦合 UI先处理输入输出边界一个容易犯的错误是把棋盘数组、搜索函数、渲染函数全部塞进同一个类看起来方便实际上你后面想换前端、想加悔棋、想调参数都会动到搜索核心。我建议后端只暴露四个能力设置棋盘、请求最佳落子、判断胜负、悔棋回退。前端只负责两件事把用户操作转换成对后端接口的调用、刷新界面。一个干净的头文件应该长这样#include vector #include utility struct EngineConfig { int max_depth 6; // 最大搜索深度 int time_limit_ms 800; // 单步超时限制 int move_range 2; // 着法生成范围 }; class ChessEngine { public: void SetBoard(const std::vectorstd::vectorint board); void MakeMove(int x, int y); void UndoMove(); std::pairint,int SearchBestMove(const EngineConfig cfg); int CheckWinner(); private: std::vectorstd::vectorint board_; };逻辑说明MakeMove和UndoMove是为了让前端不需要直接操作board_避免输入非法落子越界。SearchBestMove接收一个EngineConfig把深度、时限等参数全部外部化这样你在做“人机对弈” vs “AI 对战 AI”时可以很方便地传入不同配置。前端拿到std::pairint,int后只需要知道坐标不需要关心这个坐标是怎么算出来的。4.2 控制台前端事件循环、坐标解析和悔棋最常见的 C 五子棋源码前端是控制台简单、跨平台、好编译。控制台前端的主循环就是“读输入 → 调接口 → 刷盘”不涉及复杂的事件系统。#include iostream #include sstream #include string void run_console_frontend(ChessEngine engine) { std::string line; while (std::getline(std::cin, line)) { std::istringstream iss(line); std::string cmd; iss cmd; if (cmd move) { int x, y; iss x y; engine.MakeMove(x, y); } else if (cmd ai) { EngineConfig cfg; cfg.max_depth 6; cfg.time_limit_ms 800; auto move engine.SearchBestMove(cfg); std::cout move.first move.second std::endl; } else if (cmd undo) { engine.UndoMove(); } else if (cmd exit) { break; } } }逻辑说明用std::istringstream解析输入比scanf(%d%d)容错性强输入move 7 7、ai、undo这类命令不会因为多一个空格崩溃。ai命令会阻塞等待搜索完成如果搜索超时设成 800ms界面最多等 800ms这个延迟在人机对弈里可以接受。4.3 序列化协议坐标、棋盘、落子历史怎么传给后端如果你的前端不是 C比如是 Web 页面前后端之间就要有协议约定。最简单的是“逗号分隔棋盘快照 最新一步坐标”。后端拿到棋盘快照后生成候选着法返回“下一个落子的坐标”。// 棋盘编码0/1/2 直接拼成字符串 std::string serialize_board(const std::vectorstd::vectorint board) { std::string s; s.reserve(225); for (int i 0; i 15; i) { for (int j 0; j 15; j) { s.push_back(0 (char)board[i][j]); } } return s; }逻辑说明这种序列化格式简单、不易出错解析时按 15 个一组切回二维数组即可。注意这里默认使用“行列从 0 开始”前端点击屏幕坐标时要先把像素坐标换算回棋盘坐标换算最容易出 bug 的点是第 5 章我会讲的坐标系偏移。4.4 异步搜索后端在子线程计算前端不能冻结到了图形界面阶段最让你头大的就是搜索会把 UI 线程卡死。控制台程序无所谓等几千毫秒没感觉但 Qt 或 Web 会直接“无响应”用户以为程序崩了。解决方式是让搜索跑在独立线程前端只等待结果完成信号。#include future #include thread std::futurestd::pairint,int run_async_search(ChessEngine engine, EngineConfig cfg) { return std::async(std::launch::async, [engine, cfg]() { return engine.SearchBestMove(cfg); }); }逻辑说明std::async会新起线程执行搜索future对象可以交给界面层周期轮询或者搭配wait_for检测超时。注意 lambda 里捕获的是engine的引用你必须在 future 完成前保证engine不被析构。如果你用的是老版本编译器记得把 C 标准切到 C11 或更高这是很多“源码编译报错”的根本原因不一定是你代码写错了。4.5 前端画棋盘用什么框架不是重点撤销和重放才是重点很多 C 小游戏源码用 EasyX 或 Qt 画棋盘代码大同小异核心是维护一个“落子历史栈”。前端每次刷新界面都从历史栈重新绘制而不是增量画棋子这样可以避免撤销时还要用背景色覆盖棋子的老问题。std::vectorstd::pairint,int history_; void redraw(Board board) { clear_screen(); draw_board_lines(); for (size_t i 0; i history_.size(); i) { auto [x, y] history_[i]; draw_stone(x, y, (i % 2 0) ? BLACK : WHITE); } refresh_screen(); }逻辑说明历史栈就是“后悔药”撤销只需要pop_back()然后全量重画。这样做避免了你记录每一步要清除哪个交叉点的状态尤其适合 Web Canvas 或 Qt Widget 的重绘机制。如果你发现源码里撤销之后还残留一颗棋子多半就是没有从历史栈重绘而是用“颜色填充”硬盖。5. 避坑与排错极大极小值算法AlphaBeta剪枝的 5 个常见问题这一章我把这几年在五子棋源码里反复踩过的坑集中讲一下每一条都是“现象 → 原因 → 解决”的结构。你对照自己的代码排查能省下大量调试时间。5.1 剪枝后搜出来的着法退化了比不剪枝还差现象给negamax_search加上 alpha/beta 后AI 的棋力反而下降有时候会放弃明显的活四不走跑去堵对方一个不痛不痒的眠三。原因负极大值的实现里递归参数把alpha和beta交换并取负但更新时忘了对返回值取负。比如int score negamax_search(...)丢了前面的负号当前层拿到的分数会被当成对手视角的值alpha 更新方向反了剪枝就会剪掉真正的最佳分支。解决把递归调用写成int score -negamax_search(board, depth - 1, -beta, -alpha, 3 - player)。这是我每次调试最先核对的一行。建议写一个最简单的“双活三必胜局面”来测试AI 应该在有限深度内找到连下两步活三的路线如果找不到优先检查这一行负号。5.2 评估函数只数棋型却把“赢棋”算低了现象AI 能形成连五但它的评分不如对方一个冲四加一个活三高导致 AI 放着现成的五连不走。原因棋型分数表里连五分值设置得不够高或者评估函数对五连的判断有死角。比如五连恰好跨越到下一行时你的逐行扫描没把同一斜线上的棋子连起来。解决把连五和活四的分数设置成 1e5 和 5e4 这种“其他所有子型都追不上”的量级。写一个检查函数从每个棋子出发沿四个方向连续数同色棋子数量数量达到 5 就直接返回极大值不参与棋型累加。遇到这种边界问题固定棋局单元测试比肉眼盯盘可靠得多。5.3 搜索超时界面卡死只能强制关进程现象深度设到 6人机对弈时每次 AI 思考都要等 5~10 秒前端窗口直接变成“无响应”。原因你没有时间限制也没有迭代加深。固定深度 6 在某些局面下分支数巨大比如开局或中盘候选点特别多AlphaBeta 剪枝再快也会吃满 CPU。解决上迭代加深并把time_limit_ms设置成 800~1500ms。每一层开始前记录开始时间在走法循环里检查time_up时间到就直接返回上一层最优解。这里有个容易犯的错误只在递归最深层检查时间导致一层内部已经深陷到深分支要在最外层走法循环里检查才有效。5.4 点击棋盘落子位置偏了一个格子越下越歪现象前端画出来 15×15 棋盘点击交叉点后落子却落在相邻交叉点或者点击边缘区域越界。原因前端坐标系和棋盘数组坐标系没对齐。比如用 HTML Canvas 画图时每个格子宽度是 40 像素但你没有做“除以格子宽度再取整”的坐标换算直接把 Canvas 的像素坐标传给后端x y了。解决统一约定“后端只接受 0~14 的行列号”前端在点击事件里做int x round((mouse_x - board_left) / cell_width)。把换算函数单独拎出来写比如screen_to_board调用后端前强制走一遍。这个坑在 C Qt 里也会遇到Qt 的QMouseEvent::pos()返回的是控件坐标同样需要除以格子宽。5.5 源码用 Dev C 编译失败全是乱码和缺头文件现象下载的源码里中文注释在 Dev C 里变成乱码编译报错一堆stray ‘\241’ in program或者vector找不到。原因老版本 Dev C 默认使用 GBK 编码而源码注释可能是 UTF-8中文注释在编译时被误解析成非法字符。另外编译器默认标准可能是 C98auto、nullptr这些 C11 特性全部报错。解决用 VS Code 配置好 C/C 环境打开源码后设置编码为 UTF-8编译参数加上-stdc11。如果你习惯 Dev C在工具 → 编译器选项里把编译标准改成 C11文件另存为 UTF-8。遇到报错时先分清是编码问题还是标准问题报错全是中文乱码就是编码报错说是auto或lambda无法识别就是标准问题。6. 验证与进阶怎么确认这套搜索算法真的变强了写五子棋 AI最大的错觉是“感觉它变强了”。人机对弈几盘看不出真实的棋力因为你对局时会不自觉地放水。我后来的做法是让程序自己和自己下或者用固定棋局测试来验证用数据说话。6.1 单元测试用固定棋局测评估函数和搜索一致性把评估函数和搜索逻辑拆开测。比如摆一个“黑棋已经四连只差一步成五”的局面搜索深度 1 时应该直接返回这个落子位置。再摆一个“对方活三必须堵”的局面深度 3 时 AI 应该选择堵而不是自己乱攻。void test_block_live_three() { Board board create_board(); // 白棋活三白(7,7) (7,8) (7,9)黑方必须堵 board[7][7] 2; board[7][8] 2; board[7][9] 2; ChessEngine engine; engine.SetBoard(board); EngineConfig cfg; cfg.max_depth 3; auto move engine.SearchBestMove(cfg); // 期望堵在 (7,6) 或 (7,10) assert(move.first 7 (move.second 6 || move.second 10)); }逻辑说明这个测试的价值是约束“搜索不会因为剪枝而漏掉关键防守”。如果深度 1 能堵住深度 3 反而乱走说明你的 alpha/beta 边界传递或评估函数权重出现了回归。建议把这类测试做成一个独立小文件每次修改搜索代码就全量跑一遍比人机对弈快得多。6.2 与固定策略对弈反向调整评估权重只做单元测试还不够还要看整体棋力。我常见做法是写一个“贪心 AI”它只扫描当前棋盘优先堵对方的冲四和活三然后找自己能连四的位置。然后让搜索 AI 和它下 100 局统计胜率。如果你的极大极小搜索连贪心 AI 都稳定输问题基本出在评估函数权重或搜索深度不够。这个对局脚本可以用命令行形式跑不需要图形界面棋盘和走法全部通过ChessEngine接口交互。调权重时有个技巧先只改一个棋型的分值比如把活三从 2000 调到 3000跑 100 局看胜率变化。一次改多个参数你就不知道是谁起作用了。这个顺序调试法也适用于开局库和落子启发函数。6.3 棋谱复盘记录搜索时间、深度、剪枝率最后一个进阶技巧是给搜索加统计信息。在negamax_search外层定义两个计数器访问的总节点数和剪枝的次数。每局棋结束后打印平均剪枝率正常应该在 60% 以上低于 30% 说明候选排序函数太弱或者 alpha/beta 初始值有问题。你还可以输出每步思考耗时看哪些局面会让搜索明显变慢针对性优化着法生成范围比如把range从 2 缩到 1。我写这套东西最大的教训是不要一上来就追求深度 8 或 10。实际对战里深度 4 加好的评估函数已经能赢过大多数随手写的“贪心 AI”。深度 6 需要配合时间控制深度 8 以上你要开始做开局库和杀棋库不然纯搜索性价比会很低。先把基础版跑稳再逐步加深这样每一步都有数据支撑不会白熬夜调参。以上这些方法希望能帮到你在自己动手或者调试那套五子棋源码时少走一点弯路。本文还有配套的精品资源点击获取
返回列表