简介:VC++中国象棋人机对弈程序源码包,面向具备一定C++基础、希望深入象棋AI算法和MFC窗口开发的读者。项目提供完整工程与可执行程序,除常规棋盘规则、走法生成、局面评估外,重点实现了多类博弈搜索算法,包括Alpha-Beta剪枝、NegaScout、PVS、MTD(f)等变体,并引入置换表与历史启发式,支持残局处理,有助于观察不同搜索策略对棋力和效率的影响。压缩包共88个文件,以h/cpp源码为主,配以bmp棋盘贴图、ico/cur图标、rc资源描述、dsw/dsp工程配置及说明文档,整体约214KB,目录组织清晰,适合直接打开工程学习或二次开发。已有1724人学习下载,可作为象棋人机对战程序设计、博弈树搜索与MFC界面结合的完整实践案例,让读者快速走通从棋局表示、着法校验到AI响应的开发链路。
1. 这份 VC++ 中国象棋源代码,值钱的不只是对弈
VC++ 中国象棋人机对弈程序源代码,第一眼是再普通不过的 VC6/MFC 小项目:一个对话框、一张棋盘位图、一堆引擎文件。真正拆开才发现,这份资源最值钱的地方不在于它能把棋下到多强,而在于它把 C++ 博弈搜索的整条链路——从最朴素的 NegaMax 负极大,到带置换表和历史启发式的 NegaScout——全部以源码形式放在一个工程里,还附带编译好的 Chess.exe 和调试日志。想做 C++ 小游戏的人能拿它当 MFC 界面模板,想弄清 AI 强度差异的可以逐个引擎对着源码看差别,维护老项目的能从这套工程里看到一整套 VC6 时代的文件管理习惯。下面按我拆老工程的习惯,从骨架讲到评估,再从九种搜索引擎讲到你真正会踩的坑。
2. 工程骨架与 MFC 界面:先认 .dsw 里那四类文件
拿到任何老工程,别急着按 F7 编译,先把文件分类。这包里 .dsw、.dsp、.opt、.ncb 一眼就是 VC6 时代的工程格式,Chess.aps 是资源备份,Chess.plg 是编译日志。把这些文件按用途归成四类,后面排查问题时能省一半时间。
| 文件范围 | 类别 | 实际作用 |
|---|---|---|
| Chess.dsw / Chess.dsp / ChessDlg.dsw / ChessDlg.dsp | 工程描述 | 工程与工作区的构建入口 |
| .opt / .ncb / .plg / .aps | 本地缓存 | VC6 自动生成的用户级信息,不该进版本管理 |
| .cpp / .h | 源代码 | 界面、搜索引擎、走法生成、评估函数的实现 |
| .bmp / .rc / .txt | 资源与文档 | 棋盘位图、对话框布局、源码说明与调试记录 |
四个文件类别里,最容易被忽略的是本地缓存。我在实际维护老项目时见过不少人把 .ncb、.opt 一起提交到代码仓库,结果每次切换分支都会产生一堆无意义的冲突。这套工程里既然把它们放在一起,说明作者当时是按默认方式生成的工程,并没有刻意剥离。你下载后如果要初始化自己的版本管理,建议第一时间把这些文件加进忽略列表。
2.1 从 Chess.dsw 到 ChessDlg.cpp:入口与消息流转
工程入口是 Chess.cpp,它创建主对话框 CChessDlg。ChessDlg.cpp 负责几乎所有人机交互:解析鼠标点下的坐标,判断点中的是哪个棋子,再调用搜索引擎拿到 AI 回应,最后更新棋盘并重绘。SetDlg 是设置对话框,HelpDlg 是帮助页,这两者在老 MFC 工程里属于标配。
整个消息流转是典型的 MFC 事件驱动:用户点击棋盘 -> 坐标换算成 9x10 棋盘上的行列 -> 校验走法合法性 -> 执行走子 -> 启动 AI 搜索 -> 搜索完成回写棋盘 -> 重绘界面。这个流程里最值得留意的是“坐标换算”这一步,它和界面层直接相关,却最容易和搜索引擎内部坐标混淆。搜索引擎永远用固定的棋盘数组坐标,界面层负责把鼠标坐标换算成数组下标,两层各管各的,职责才不会乱。
2.2 双缓冲、自绘按钮与双棋盘:界面层的三个细节
MemDC.cpp/h 是典型双缓冲实现,作用是消除棋盘重绘时的闪烁;XPButton.cpp、CoolButton.cpp 是两套自绘按钮;GradientProgressCtrl 是渐变进度条,用于 AI 思考时显示搜索进度。一个象棋程序能配进度条,说明作者把搜索放到了后台线程,否则窗口会直接卡死。
资源目录里同时存在“棋盘正.bmp”和“棋盘倒.bmp”,这对应红方视角和黑方视角。人机对弈时,如果 AI 执黑,用户视角下的棋盘保持正置,AI 思考时则使用倒置位图,保证走棋方棋子在下。常见做法是界面层根据当前行棋方切换位图,而搜索引擎不感知任何翻转。
提示:棋盘翻转只发生在界面层,搜索引擎内部始终用固定坐标。把翻转逻辑写进搜索层,是新手最容易埋下的坐标错乱隐患。
2.3 MoveGenerator:棋盘数组、马腿和炮架的状态机
棋盘用 10 行 9 列的二维数组表示,棋子编号是 0=空、1~7=红方、8~14=黑方。MoveGenerator.cpp/h 负责生成所有合法走法,是整个引擎里第一个要写对的文件。象棋里最容易写歪的是马和炮:马要处理蹩马腿,炮要处理隔子吃。
// 棋盘定义与棋子编号(按该工程常见约定) const int BOARD_H = 10; const int BOARD_W = 9; BYTE board[BOARD_H][BOARD_W]; // 0=空,1=红帅,2=红仕,3=红相,4=红马, // 5=红车,6=红炮,7=红兵,8~14 为对应黑方 // 马走“日”:八个跳步,每个跳步对应一个蹩腿位置 static const int horse_dx[8] = { -2, -1, 1, 2, 2, 1, -1, -2 }; static const int horse_dy[8] = { -1, -2, -2, -1, 1, 2, 2, 1 }; static const int leg_dx[8] = { -1, 0, 0, 1, 1, 0, 0, -1 }; static const int leg_dy[8] = { 0, -1, -1, 0, 0, 1, 1, 0 }; void MoveGenerator::gen_horse_moves(int x, int y) { for (int i = 0; i < 8; i++) { int nx = x + horse_dx[i]; int ny = y + horse_dy[i]; if (nx < 0 || nx >= BOARD_W || ny < 0 || ny >= BOARD_H) continue; // 蹩马腿:leg 位置有任意棋子都走不了 if (board[y + leg_dy[i]][x + leg_dx[i]] != 0) continue; // 目标格为空,或者为对方棋子时都可走 if (board[ny][nx] == 0 || is_opponent(board[ny][nx])) { add_move(x, y, nx, ny); } } }这段代码里最关键的是蹩腿表必须与跳步表索引一一对应。leg_dx[i]、leg_dy[i] 是 horse_dx[i]、horse_dy[i] 的二分之一,索引错一位,马就能“飞过”棋子,这是走法生成器里最经典的隐 bug。另外 is_opponent 的判断不能漏,否则马会把己方棋子当目标吃。
// 炮:直线上走,吃子时必须且只能隔一个“炮架” void MoveGenerator::gen_cannon_moves(int x, int y) { const int dir[4][2] = { {0,1}, {0,-1}, {-1,0}, {1,0} }; for (int d = 0; d < 4; d++) { int nx = x + dir[d][0], ny = y + dir[d][1]; int jump = 0; // 0=无炮架,1=已隔一个 while (nx >= 0 && nx < BOARD_W && ny >= 0 && ny < BOARD_H) { if (jump == 0) { if (board[ny][nx] != 0) jump = 1; // 遇到棋子,当作炮架 else add_move(x, y, nx, ny); } else { if (board[ny][nx] != 0) { // 隔一个后第一个子 if (is_opponent(board[ny][nx])) add_move(x, y, nx, ny); break; // 此方向到此为止 } } nx += dir[d][0]; ny += dir[d][1]; } } }炮的代码核心是 jump 状态机。jump=0 时只走不吃,遇到棋子后切换状态;jump=1 时继续向前扫描,遇到第一个棋子才判断能否吃,然后无论是否吃子都 break。不少初版实现会犯“隔几个都能吃”的错误,问题就出在 break 的位置。走法生成器一旦有错,搜索引擎再强也只是在错误的树里找最优解。
3. 评估函数 Eveluation.cpp:文件名拼错,方向不能弄错
如果你按正常拼写去搜“Evaluation.cpp”,在这个工程里是搜不到的——文件名叫 Eveluation.cpp,少了一个字母 i。老工程里这种命名不规范很常见,不影响编译,但提醒我一件事:文件名可以错,评估的方向约定不能错。
评估函数的作用是对任意局面给出一个分数,搜索引擎靠它判断哪条路更好。它处于搜索树的最底层,每次 hit 叶子节点都会调用。评估函数的质量直接决定“同一个搜索深度下,棋力差多少”。
3.1 子力价值与位置分值:评估是搜索的地基
先看最基础的子力价值表。价值单位通常以兵为基准,车、马、炮、士、相、帅依次放大。
// 以兵=100 为基准的子力价值,帅的值必须远大于所有棋子总和 // 但不能超过搜索层里的 INF(一般取 30000) static const int PIECE_VALUE[16] = { 0, // 0: 空 10000, // 1: 帅 / 将 350, // 2: 仕 / 士 280, // 3: 相 / 象 400, // 4: 马 550, // 5: 车 480, // 6: 炮 100, // 7: 兵 / 卒 // 8~14 黑方与红方同值 }; // 常见做法:评估函数固定返回“红方视角”分数,红正黑负 int evaluate(const BYTE board[BOARD_H][BOARD_W]) { int score = 0; for (int y = 0; y < BOARD_H; y++) { for (int x = 0; x < BOARD_W; x++) { BYTE p = board[y][x]; if (p == 0) continue; int v = PIECE_VALUE[p >= 8 ? p - 8 : p]; score += (p >= 8) ? -v : v; // 黑负红正 } } return score; }这段代码里值得注意的有两点。第一,帅的值取 10000 是有讲究的:它必须大于所有棋子价值之和(理论最大约为 5504+4802+4002+1005+3502+2802 ≈ 4660),这样任何吃帅的走法在评估上都是绝对优势;同时它又要小于搜索层内部的 INF,否则将死判断会和普通优势混淆。第二,p>=8 的判定是区分红黑的常用约定,下面 3.2 会讲它如何与搜索引擎协作。
真正的强评估不会只看子力。每个兵种通常还会配一张位置分值表,马在边角和在河沿的价值差距很大,兵过河之后才真正值钱,车被压在自己底线和展开到肋道也是两个世界。这类位置表在 Eveluation.cpp 里一般以静态数组形式存在,按棋子的兵种和坐标取值。位置表是经验值,可以边跑边调,但方向比数值重要——数值差几十个点影响不大,方向取反则是毁灭性的。
3.2 红正黑负:side 约定一旦混了,棋力直接腰斩
搜索引擎里有个 side 参数,表示当前轮到谁走。评估函数若能固定返回红方视角分数,那么搜索层内部只需要按 side 简单翻转。
// 搜索引擎调用评估的统一入口 // RED=1, BLACK=-1,side 表示当前行棋方 int eval_for_side(const BYTE board[BOARD_H][BOARD_W], int side) { // evaluate() 固定返回红正黑负,按行棋方翻转 return (side == RED ? 1 : -1) * evaluate(board); }这个翻转如果漏了,会出现一个很隐蔽的后果:黑方在搜索时反复选择对红方有利的局面,因为评估函数永远在夸红方、贬黑方,黑方等于在帮对手找棋。整棵树搜索完后,AI 会走出完全混淆的走法。我在调试其他棋类引擎时见过这种“AI 疯狂送子”的现象,最后定位就是 side 没有乘进去。
INF 的取值也要配套处理。将死的分数在评估里通常用 INF 减去搜索深度表示,保证“越快将死分数越高”。如果 INF 取 30000,而帅的价值是 10000,那么搜索层里的杀棋分(接近 30000)永远高于任何子力分,这个层级关系不能乱。
3.3 将帅安全与残局:为什么 PVS 文件名里带着“残局”
评估函数除子力外,还要考虑将帅安全。将帅在九宫内是否被牵制、身边有没有士相保护、是否暴露在车炮射程下,这些都会影响真实局面分。文件清单里“残局 PVS_Engine.cpp”这个命名很有信息量——作者把 PVS(主变例搜索)定位在残局场景使用。残局的特点是子力少、搜索深度可以很深、主变例相对稳定,恰好是 PVS 的零窗口试探策略收益最大的地方。
评估要达到“残局可用”,光靠子力分不够,还要处理兵的位置权重变化。残局阶段一个过河兵的价值可能从中局的 100 涨到 300 以上,接近一个马。所以正规一点的评估函数会在局面不同阶段切换不同的位置表,或者按双方剩余棋子总价值判断当前处于开局、中局还是残局。这个工程没有把阶段切换做得特别复杂,但它把 PVS 单独列成一个引擎文件,已经说明作者意识到了残局需要不同的搜索策略。
4. 搜索引擎族谱:从 NegaMax 到 MTD(f) 的九档强度
这套工程最让我意外的是,它一次性集齐了九种搜索引擎。大多数象棋爱好者自己写引擎,通常只会写一版 Alpha-Beta 就收工,而这套源码把 Alpha-Beta 从裸剪枝到配历史启发式、配置换表、再进化成 NegaScout 和 MTD(f) 整个过程都留了一份实现。不夸张地说,读这九个文件等于把博弈搜索的发展史读了一遍。
4.1 负极大加 Alpha-Beta 的 C++ 骨架
先看最核心的搜索函数。负极大(Negamax)的要点是:把“对手视角”统一成“自己视角”,每一层递归都取负号,配合 Alpha-Beta 剪枝就能大幅减少搜索节点。
// 负极大 + Alpha-Beta 剪枝的经典骨架 // depth 是剩余搜索深度,alpha/beta 是窗口,side 是当前行棋方(1/-1) int search(BYTE board[BOARD_H][BOARD_W], int depth, int alpha, int beta, int side) { // 叶子节点用评估函数打分,side 乘进去得到本层视角的分值 if (depth == 0) return side * evaluate(board); MoveList moves; MoveGenerator::generate(board, moves); // 走法排序:高质量走法先搜,剪枝效率成倍提升 // 历史启发式(HistoryHeuristic.cpp)和吃子价值是主要排序依据 moves.score_by_history_and_capture(); moves.sort_desc(); int best = -INF; for (int i = 0; i < moves.count; i++) { make_move(board, moves[i]); int val = -search(board, depth - 1, -beta, -alpha, -side); unmake_move(board, moves[i]); if (val > best) best = val; if (val > alpha) alpha = val; if (alpha >= beta) { // 剪枝:当前分支已经不可能影响最终决策 HistoryHeuristic::update(board, moves[i], depth); break; } } return best; }这段代码是整套搜索引擎的地基,几个参数必须理解到位。depth 控制搜索深度,每递归一层减一,depth=0 时落到评估函数。alpha 是当前层已知的最佳分下界,beta 是对手能接受的最差上界。负极大取反的写法是这行:-search(board, depth-1, -beta, -alpha, -side),窗口和 side 都要取反,顺序不能乱。剪枝条件 alpha >= beta 成立时,说明当前走法已经让对手没有反制空间,这条分支不用再搜。HistoryHeuristic::update 在剪枝发生时记录这个走法,深度越深权重越高,供后续节点排序使用。
4.2 九个引擎文件,九种搜索策略
把九个引擎文件排成一张对照表,结构和定位一目了然:
| 引擎文件 | 搜索机制 | 记忆结构 | 实战定位 |
|---|---|---|---|
| NegaMaxEngine.cpp | 负极大,无剪枝 | 无 | 算法演示,教学向 |
| AlphaBetaEngine.cpp | Alpha-Beta 剪枝 | 无 | 入门可用的最小实现 |
| Alphabeta_HH.cpp | Alpha-Beta + 历史启发式 | HH 表 | 改善走法排序,剪枝效率提升 |
| AlphaBeta_TTEngine.cpp | Alpha-Beta + 置换表 | TT 表 | 消除重复搜索同一局面 |
| IDAlphabetaEngine.cpp | 迭代加深 | 无 | 时间可控,随时可用上一层结果 |
| AspirationSearch.cpp | 渴望搜索 | 无 | 窄窗口试探,失败后扩大重搜 |
| PVS_Engine.cpp | 主变例搜索 | 无 | 残局深搜稳定,零窗口探测 |
| NegaScout_TT_HH.cpp | NegaScout + TT + HH | TT+HH 双表 | 综合最强,工程主推 |
| MTD_fEngine.cpp | MTD(f) 零窗口搜索 | TT 表 | TT 命中率高时速度极快 |
从这九个文件能清楚看到一条演进线:裸负极大太慢,加剪枝;剪枝依赖走法顺序,加历史启发式;同一局面被反复搜索,加置换表;固定深度不实用,加迭代加深;渴望搜索和 PVS 是 Alpha-Beta 的窗口变体;MTD(f) 则把搜索退化成一系列零窗口探测,完全依赖置换表质量。
强度这种东西不是玄学,而是每一层机制叠加的结果。但要注意,引擎强度排序不是绝对的:MTD(f) 在置换表实现不当时,表现反而不如普通 Alpha-Beta,因为零窗口探测需要极高的 TT 命中率才有意义。这也是为什么工程里保留了多种引擎——不同局面、不同引擎阶段,选型是有讲究的。
4.3 搜索引擎与界面怎么接:SearchEngine 和后台搜索
SearchEngine.h/cpp 在这套工程里承担接口层职责。界面不直接调用某个具体引擎,而是通过 SearchEngine 统一入口发起搜索。用户走完一步后,ChessDlg 启动一次后台搜索,搜索完成后把最佳走法回写到棋盘。工程里有 GradientProgressCtrl 进度条控件,说明作者把搜索放到了独立线程,否则界面会在 AI 思考时假死。
// 按常见 MFC 象棋工程的调度方式示意 void CChessDlg::OnUserMove(int fromX, int fromY, int toX, int toY) { MakeMove(fromX, fromY, toX, toY); // 后台执行搜索,深度由当前难度档决定 m_searchThread.Start(depthPerLevel[m_settings.m_level]); }这段逻辑里的 m_settings.m_level 来自 SetDlg 设置对话框,不同难度档对应不同引擎类型和搜索深度。工程把难度选择、引擎选择、深度配置拆开了,这是比较规范的架构,也方便你针对其中某一档单独调参。
5. 避坑清单:老棋谱在 Win10 上跑不起来,先查这五处
这套工程有编译好的 Chess.exe,也有完整源码,但你在新系统上跑起来之前,大概率会撞上下面五个坑。前三道是“能不能跑”,后两道是“跑起来后棋力对不对、界面对不对”,全部是我在实际迁移老项目时反复遇到的。
5.1 双击 Chess.exe 报缺 DLL:VC6 运行库并不在当前 Redistributable 里
现象:在 Win10/11 上双击 Chess.exe,提示“无法启动此程序,因为计算机中丢失 MSVCP60.dll”或“需要安装 Visual C++ Redistributable”。
原因:VC6 编译出来的程序默认动态链接 VC6 时代的 C 运行时库。而现在微软官方分发的 Visual C++ Redistributable 覆盖的是 2005 年以后的各版本,VC6 那套运行库不在其中,装最新的 Redistributable 也补不上。
解决:装 VC6 运行库安装包;或者干脆用 VS2008(VC9)重新编译一次工程,让产物链接新版运行时。想知道本机装了哪些 Redistributable,可以用命令查:
reg query "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" /s | findstr /i "redistributable"这条命令会把已安装列表里所有 Redistributable 条目列出来,快速确认缺的是哪个版本。相比一个一个装全家桶,先查再装效率高得多。
5.2 新版 IDE 升级 .dsw 全红:不要和编译器版本较劲
现象:用 VS2019/2022 打开 Chess.dsw,向导提示转换,转换后编译直接冒出来上百个 C4996、C2664 错误。
原因:.dsw 是 VC6 的工程格式,新版 IDE 会先做工程迁移。而 MFC 老代码大量使用 strcpy、CString 隐式转换等旧写法,新版工具集对这类写法不再宽容,头文件一变,错误成片出现。
解决:老工程配老编译器最省心。VS2008(VC9.0)打开这套代码基本是零迁移成本,这也是 Visual Studio 2008 到现在还在被反复提起的原因。想用 VSCode 配置 C/C++ 环境来读代码完全可行,但 VSCode 解决不了 MFC 库依赖,真要出 exe 还得走原工具链。与其花一下午迁移工程,不如装个 VS2008 一步到位。
5.3 有剪枝却等于全搜:窗口取反写错是 Alpha-Beta 的隐形杀手
现象:Alpha-Beta 加进去之后,搜索节点数没有明显下降,甚至要把 depth 调小才能跑出结果。
原因:最经典的写法错误是负极大取反时窗口顺序写错。正确写法是 -search(depth-1, -beta, -alpha, -side),写成 -search(depth-1, -alpha, -beta, -side) 的话,beta 永远不变,alpha >= beta 的剪枝条件形同虚设,整棵树还是会全部搜完。这个错误编译期零感知,运行时只是慢,很难定位。
解决:在 search 函数入口加一个静态计数器,统计搜索过的节点数。对比剪枝开启前后同一深度下的节点数,如果几乎没差,基本就是窗口取反的顺序问题。剪枝条件成立时打一条日志,观察 alpha、beta 的变化轨迹,几层之后窗口没收窄,问题就在取反。这是我见过最多次的“看着写了剪枝实际全搜”的翻车现场。
5.4 置换表命中却棋力倒退:TT 里的深度和类型比命中率重要
现象:开了置换表后,节点数明显下降,但棋力没提升,个别局面还会出现“绕远路”的怪棋。
原因:节点数下降只说明重复搜索变少了,但置换表条目能复用是有条件的。如果无条件覆盖,浅层搜索的上界值会冲掉深层搜索的精确值,导致引擎在关键局面上做出错误判断。只看命中率不看深度和分数类型,等于把缓存当黑匣子用。
解决:TT 条目至少保存五个字段:哈希键、搜索深度、分数、分数类型、最佳走法。分数类型要区分精确分、上界、下界三种,这是 MTD(f) 和 NegaScout 能工作的前提。
struct TTEntry { uint64_t key; // zobrist 哈希键,用来判断命中 int depth; // 搜索深度 int score; // 返回值 int flag; // 0=精确分,1=上界,2=下界 int bestMove; };读取时要求 ttDepth >= depth 才可用,写入时只有新深度大于等于原深度才覆盖。零窗口搜索的本质就是不断探测上下界,flag 写错,整套机制都会失效。
5.5 棋盘闪烁与残影:重绘方式不对,双缓冲等于白做
现象:拖动棋子时棋盘闪烁,AI 走完一步后棋盘上留有残影。
原因:重绘时用了 Invalidate(TRUE),把整个客户区背景擦掉再画。双缓冲的要求是所有绘制先画到内存 DC,最后一次 BitBlt 拷贝到屏幕,中间如果混入直接对窗口 DC 的绘制,双缓冲就被破坏了。Invalidate(TRUE) 擦背景的动作就是破坏源头。
解决:改用 Invalidate(FALSE),只刷新棋盘区域不擦背景;绘制代码整块走 MemDC,一次拷贝完成。另外“棋盘正.bmp”和“棋盘倒.bmp”对应两方视角,AI 走完一步要同步切换位图,否则棋子位置正确但棋盘方向不对,操作感会非常别扭。
6. 验证与调参:用深度、节点数和互弈确认 AI 变强
改完引擎,最怕的就是“感觉快了”。要确认改动有效,离不开三个指标:搜索深度、节点数、互弈胜率。
| 指标 | 怎么测 | 看什么 |
|---|---|---|
| 搜索深度 | 固定同一局面,调整 depth 4/6/8 | 时间是否可控,棋风是否变化 |
| 节点数 | 搜索入口加计数器 | 剪枝前后同深度节点数是否下降 |
| 互弈胜率 | 两个引擎相同深度互下固定开局 | 胜率超 70% 才算质变 |
深度增加 1,理论节点数会膨胀数倍。如果 depth 从 4 提到 5,节点数涨了不到 1 倍,要么是剪枝生效极好,要么是走法生成器漏了走法。节点数统计是最快暴露问题的杠杆。
6.1 三个可量化的验证手段
固定局面调深度是最直接的手段。挑一个中局局面,分别用 depth=4、6、8 跑一遍,记录耗时和最佳走法。如果 depth=8 的走法和 depth=4 一样,说明搜索引擎的问题大概率出在评估函数而不是搜索深度。节点数方面,我会在 search 入口加计数,剪枝前后对比同深度的节点总量,下降 3 倍以上才算正常。互弈胜率是最能说服旁人的数据,让修改前和修改后的引擎在相同条件下对弈 20 盘,稳定胜率超过 70%,这个改动才算站得住。
6.2 一个值得试的调参习惯:时间控制优先于固定深度
固定 depth=6 在中局可能严重超时,在残局又浪费算力。更好的做法是时间控制加迭代加深:每搜完一层看剩余时间,超时就用上一层的结果。工程里 IDAlphabetaEngine 和 AspirationSearch 的存在意义就在于此——短时限内先浅层快速试水,时间富余再加深。迭代加深还会带来一个隐藏收益:浅层搜索的最佳走法可以作为深层的首步走法,走法排序质量更高,剪枝效率自然更好。
我拿到这套工程后,习惯先打开根目录的源码说明.txt 和调试.txt,把作者自己记的调试记录过一遍再动手编译。有一回我跳过调试.txt 直接改迭代加深的层间排序,跑出来的对弈表现反而不如原版,回看记录才发现作者早写过“残局深度过大容易回退”这几个字。从那以后每次接触老工程,我都强制先读说明再动手,希望帮到你。
本文还有配套的精品资源,点击获取