资讯动态

VB中国象棋棋力惊人:Alpha-Beta剪枝与评估函数实战解析

发布时间:2026/10/14 15:21:38 来源:尧图企业网站定制
简介一款基于VB开发的中国象棋人机对弈项目面向VB学习者、算法爱好者与象棋玩家完整展示从图形界面搭建到AI引擎实现的开发流程。压缩包共725个文件约5.35MB包含编译好的可执行程序、VB窗体/类模块/标准模块源文件frm/bas/class、棋盘棋子与图标素材gif/png/ico、棋谱数据epd/pgn/xqf及过程音效wav/mid另有数款棋盘主题包可替换界面目录划分清楚。目前已有561人下载学习。内置棋力惊人的AI引擎采用深度搜索与剪枝策略配合开局库、中局战术与残局库支持多档难度除严谨的规则实现外还提供悔棋、提示、保存/加载对局等实用功能。解压后可获得完整VB工程与可运行程序便于研究事件驱动编程、走法生成、局面评估和经典博弈搜索算法也可直接作为对弈练习、毕业设计或课程设计的参考。1. 看到一个 VB 象棋程序号称「棋力惊人」先别急着笑第一次在项目简介里看到「vb中国象棋棋力惊人」这个标题某开发者的第一反应多半是VB 做界面还行写象棋 AI 能强到哪去等真正把它跑起来惊讶的方向反过来了——一个老牌的 VB6 程序中局思考能稳定摸到 5 到 6 层读秒阶段抓杀棋快得离谱业余棋手坐下来认真下往往撑不到残局就丢了子力。标题里的「棋力惊人」根本不在棋盘画得多好看而在那一套用纯 VB 代码堆出来的博弈搜索走法生成、Alpha-Beta 剪枝、静态搜索、评估函数。这篇笔记就把这套东西拆开讲清楚适合想搞懂象棋程序棋力来源、打算自己复现一个能打引擎的开发者。新手可以照着把棋盘和主搜索跑通老手则重点关注评估参数和几个反直觉的坑。2. 棋盘表示与走法生成棋力惊人的第一块地基走法生成是搜索的前提这一步错了后面搜索深度再高都是在错误分支上浪费时间。中国象棋棋盘 10 行 9 列共 90 个交叉点数据结构的选择直接决定后续递归搜索的写法和速度。很多从国际象棋程序转过来的人习惯用位棋盘但在 VB 里位运算和 64 位变量支持都不顺手最常见的可靠方案还是一维字节数组加行列索引换算。2.1 棋盘怎么存90 点一维数组与行列转换我一般用Board(0 To 89)一个一维数组index row * 9 col做行列到下标的换算。棋子用 Byte 存0 表示空位红方用 1 到 7黑方用 11 到 17这样判断敌我只需要比较数值区间不需要额外的颜色数组。Const ROWS As Integer 10 Const COLS As Integer 9 Const TOTAL As Integer 90 Dim Board(0 To TOTAL - 1) As Byte Dim Side As Integer 1 表示红方走-1 表示黑方走 行列转下标 Private Function Idx(ByVal r As Integer, ByVal c As Integer) As Integer Idx r * COLS c End Function 编码约定 1帅 2仕 3相 4马 5车 6炮 7兵 11将 12士 13象 14马 15车 16炮 17卒为什么不用二维数组Board(9, 9)二维数组在做方向遍历时代码更直观但 Alpha-Beta 搜索里走子和悔子操作极其频繁一维数组配合手工行列换算循环访问更连续VB 里执行效率略高而且用CopyMemory做整盘快照后悔棋也方便。棋子编码必须固定评估函数和走法生成都要依赖它中途改编码是给自己埋雷。这里有一个关键选择搜索时每次走子用「拷贝整盘棋盘」还是「走子再悔子」VB 数组直接赋值会整盘复制开销大正确做法是MakeMove落子、记下被吃子和原位置搜索完UnmakeMove还原。整个引擎只有这一套棋盘在反复改速度差距能到两三倍。2.2 走法生成炮的翻山、马的蹩腿、象的塞眼与将帅照面走法生成是引擎里最容易写错的部分。车的走法简单直线扫描到第一个子为止炮要走两段逻辑——先找空点碰到第一枚棋子后继续往前走遇到第二枚棋子才能吃。马要查蹩马腿象要查塞象眼士和帅限制在九宫内兵没过河只能向前、过河后才能横走。这些规则每一个都是一行判断但合在一起很容易漏。以炮的走法为例核心是「翻山」状态机 生成一枚炮从 idx 出发的全部可落点 Private Sub GenCannonMoves(ByVal idx As Integer, ByVal side As Integer, ByRef mvList As MoveList) Dim r As Integer, c As Integer Dim dr As Integer, dc As Integer Dim crossed As Boolean Dim i As Integer For i 0 To 3 dr DirR(i): dc DirC(i) 上下左右四个方向增量 r RowOf(idx): c ColOf(idx) crossed False Do r r dr: c c dc If r 0 Or r ROWS Or c 0 Or c COLS Then Exit Do If Not crossed Then 没翻山之前空格可走遇到第一枚子则进入翻山状态 If Board(Idx(r, c)) 0 Then AddMove mvList, idx, Idx(r, c), 0 Else crossed True End If Else 翻山之后遇到的第一个子就是炮的吃子目标 If Board(Idx(r, c)) 0 Then If IsEnemy(side, Board(Idx(r, c))) Then AddMove mvList, idx, Idx(r, c), 1 End If Exit Do End If End If Loop Next i End Sub这段代码里DirR和DirC是预先定义的方向增量数组AddMove把起点、终点、是否吃子三个信息记进走法列表。炮翻山最容易错的地方是crossed标志的复位——每个方向重新开始扫时都必须重置为 False否则第二个方向会沿用上一个方向的翻山状态生成出完全不合法的走法。马、象、士这些走法生成方式类似区别只在步长和中间阻挡点的判断推荐统一走「先生成候选步再统一过滤合法性」的路线。合法性过滤有一个绕不开的规则将帅不能照面走完一步棋后己方将帅不能处于被攻击状态。最稳的实现方式是「先落子、再检查己方将帅是否被攻击」——候选走法全部生成MakeMove之后调用IsInCheck(side)被将军就把这一步从列表里剔除。这样做的效率比在走法生成阶段逐条排除略低但只要IsInCheck写得够精简整体耗时完全可以接受。不合法走法宁可少生成一条也不能多放进来一条否则搜索越深翻车越狠。2.3 用起始局验证走法生成红方应该正好 22 种走法生成写完先别急着接搜索拿初始局面测一遍数字。红方初始局面合法走法应该正好是 22 种帅 1、仕 2、相 2、马 2、车 4双车各两个竖向前进点、炮 6、兵 5。黑方对称也是 22全盘合计 44。Private Sub DebugInitialMoves() InitBoard() 摆出初始局面 Dim total As Long, i As Integer For i 0 To TOTAL - 1 If Board(i) 1 And Board(i) 7 Then total total CountLegalMoves(i, 1) End If Next Debug.Print 红方初始合法走法: total End SubInitBoard按标准棋谱把 32 枚棋子摆到初始位置CountLegalMoves对单个棋子生成走法后按 2.2 说的合法性过滤再计数。如果你跑出来的数字不是 22优先检查两处炮的翻山状态有没有跨方向串扰兵的过河判定用的是行号方向还是颜色方向。这两个位置是初学者最常踩的坑数字对不上时先怀疑它们。这个 22/44 的校验锚点能帮你确认走法生成没问题再进入搜索层不然在错误走法上做 Alpha-Beta 等于在流沙上盖楼。3. 搜索与评估棋力惊人的真正心脏走法生成只是把所有选项摆出来棋力来自「在有限时间内从这些选项里挑出最好的一步」。这部分由评估函数和 Alpha-Beta 搜索共同决定。评估函数告诉程序一个局面值多少分搜索负责向前推演若干步找到最值得走的路线。「棋力惊人」的秘密基本都在这两层里。3.1 评估函数子力分、位置分与将帅安全评估函数的第一层是子力价值。象棋程序里经典的比例是车最重、马炮次之、相仕和兵最轻。一组经过大量实战验证可用的基础分如下棋子基础分说明帅/将30000对比所有子力总和还要高一个量级车1000全盘最重要的进攻子马460位置好可再上浮 10%炮450残局阶段炮价值往往略降相/象200残局可保留防守加分仕/士200保帅的功能价值兵/卒100过河后逐步加至 250第二层是位置分。兵的位置分变化最大未过河 80 到 100过河后 180 到 250逼近九宫时最高能到 350。马怕被蹩腿所以中路和八面威风的位置要加分边马要扣分。炮在己方阵地和中路有炮架时价值更高。位置表可以用一个90格数组预存评估时查表。Private Function Evaluate(ByVal side As Integer) As Long Dim score As Long, i As Integer For i 0 To TOTAL - 1 If Board(i) 0 Then GoTo NextPiece If Board(i) 7 Then 红方子从红方角度加分 score score PieceValue(Board(i)) score score PosValue(i, 1) Else 黑方子从红方角度减分 score score - PieceValue(Board(i) - 10) score score - PosValue(i, -1) End If NextPiece: Next i If side 1 Then Evaluate score Else Evaluate -score End FunctionPieceValue查基础子力分PosValue查位置表黑方用Board(i) - 10把 11 到 17 映射回 1 到 7 再查同一个表。最后一行按当前走棋方返回正负让搜索层统一用「最大化自己分数」的视角这是负极大值搜索的写法基础。将帅安全性体现在哪里如果你只做子力和位置分会出现帅暴露在开阔地带的局面仍然被评成正面分。常见做法是检测将帅周围是否有己方子保护有加 20 到 30九宫中心空虚则扣分。这层影响不像子力分那么直接但残局阶段差异非常明显。3.2 Alpha-Beta 剪枝把 5 层搜出 7 层的感觉全宽度搜索做 5 层中国象棋中间阶段每层约有 40 种合法走法全展开就是40^5量级任何语言都扛不住。Alpha-Beta 剪枝的核心一点也不神秘搜索过程中维护一个上界beta一个下界alpha一旦某条分支的分数超过当前beta这层后面的走法就不用再看了。Private Function AlphaBeta(ByVal depth As Integer, ByVal alpha As Long, _ ByVal beta As Long, ByVal side As Integer) As Long If depth 0 Then AlphaBeta Quiescence(alpha, beta, side) Exit Function End If Dim best As Long, val As Long, i As Integer best -999999 GenerateMoves side, mvList 走法已经按吃子优先/历史分排序 For i 0 To mvList.Count - 1 MakeMove mvList(i) If IsInCheck(side) Then 走后己方被将军该步非法 UnmakeMove mvList(i) GoTo NextMove End If val -AlphaBeta(depth - 1, -beta, -alpha, -side) UnmakeMove mvList(i) If val best Then best val If val alpha Then alpha val If alpha beta Then Exit For beta 截断剪枝发生在这里 NextMove: Next i AlphaBeta best End Function用负极大值写法时递归调用传-beta, -alpha, -side返回值取负数这样红黑双方共用同一个搜索函数。三个关键点决定了这套代码的实际棋力第一MakeMove后必须做IsInCheck合法性检查不走这条引擎会经常走出送帅的棋第二走法排序极端重要吃子走法排最前、历史表里上次表现好的走法排前面排序好的情况下剪枝率能到 90% 以上同样 3 秒时间内比乱序搜索多探一层多第三alpha beta的退出条件要放在更新alpha之后顺序写反会导致剪枝失效搜索时间指数级增长。这一段的血泪经验是走法排序没做之前先别急着调评估参数「棋力惊人」的引擎几乎都在排序上下了功夫。3.3 迭代加深与静态搜索水平线效应的后悔药固定深度搜索有个天然缺陷如果杀棋序列恰好跨过深度边界引擎就看不到那次将军。迭代加深和静态搜索是解决这个问题的标准方案。迭代加深是从深度 1 开始逐层加深搜索每一层把上一步的最佳走法记录下来传给下一层做排序。直接搜深度 6 和从 1 逐层搜到 6后者的总时间反而更短因为前面的浅层搜索为深层提供了高质量排序剪枝效率大幅提升。Private Function SearchRoot(ByVal maxDepth As Integer, ByVal maxTimeMs As Long) As Long Dim bestMove As Long, d As Integer, startTick As Long startTick GetTickCount For d 1 To maxDepth If GetTickCount - startTick maxTimeMs Then Exit For bestMove AlphaBeta(d, -999999, 999999, Side) StorePVMove bestMove, d 记住本层最佳走法供下一层排序 Next d SearchRoot bestMove End FunctionmaxDepth设置上限maxTimeMs是思考时间预算时间到了就不再加深返回当前已经搜出来的最佳走法。这个「时间优先、深度弹性」的机制比你设定固定深度 6 而不管程序会不会超时可靠得多。StorePVMove把每层主变Principal Variation的第一步行记录到一个全局变量下一层搜索时把这一步排在最前面这是迭代加深能加速的核心。静态搜索负责处理搜索边界的不安定局面。当depth 0时先快速评估当前局面的分数standPat安分分数如果引擎在这一层已经超过beta就直接返回否则只展开吃子走法继续搜几步直到没有吃子可走才返回评估值。这样能有效避免「对方下一步要吃我的车但搜索恰好在吃之前截断」的近视情况。Private Function Quiescence(ByVal alpha As Long, ByVal beta As Long, _ ByVal side As Integer) As Long Dim standPat As Long, val As Long, i As Integer standPat Evaluate(side) If standPat beta Then Quiescence standPat Exit Function End If If standPat alpha Then alpha standPat GenerateCaptureMoves side, capList 只生成吃子走法数量远小于全走法 For i 0 To capList.Count - 1 MakeMove capList(i) val -Quiescence(-beta, -alpha, -side) UnmakeMove capList(i) If val beta Then Quiescence val Exit Function End If If val alpha Then alpha val Next i Quiescence alpha End Function静态搜索最大的坑在于「只生成吃子走法」这个限制。如果贪心地把所有走法都放进静态层叶子数量会爆炸到比主搜索还慢。正确的做法就是GenerateCaptureMoves只生成吃子序列必要时可加入将军延展但要设一层上限避免吃子交换链无限深入。这套组合拳打完引擎在中局的表现会有明显的阶跃式提升这就是标题里「棋力惊人」四个字的真正技术来源。4. 把引擎装进程序从递归函数到能下棋的界面搜索算法写完只是核心要让一个普通用户能「点一下、下一步棋」还得把引擎和界面接起来。这部分的常见失败不是算法错而是 VB6 单线程模型导致界面假死、走法输入不合法却没拦住、悔棋恢复错局面。4.1 引擎与界面解耦一个函数解决「电脑下一步怎么走」引擎最外层只需要一个入口函数给定当前局面和思考时间返回一步走法。界面层不需要知道 Alpha-Beta 怎么递归、Quiescence 怎么展开它只认from和to两个坐标。这种解耦能让你单独测试引擎——先不画棋盘直接命令行输出每步走法跑通了再接界面。 引擎入口返回形如 from * 90 to 的走法编码 Private Function GetComputerMove(ByVal maxTimeMs As Long) As Long GetComputerMove SearchRoot(MaxDepth, maxTimeMs) End Function 界面状态机 Private Sub Timer1_Timer() Select Case GameState Case 1 等待我方下棋 Exit Sub Case 2 AI 思考中 If aiBusy Then Exit Sub aiBusy True aiMove GetComputerMove(thinkMs) MakeMove aiMove RefreshBoard() aiBusy False GameState 1 End Select End SubGameState控制在玩家和 AI 之间切换aiBusy标志防止 Timer 事件在 AI 思考期间被重复触发。VB6 的 Timer 控件有自己的时间片但如果 AI 搜索在递归里全程占用 CPUTimer 事件实际上挤不进来界面照样会卡。常见做法是在SearchRoot的迭代加深循环每层之间调用一次DoEvents让系统有机会处理界面刷新和 Timer 消息。注意DoEvents不能放到 Alpha-Beta 最内层否则频繁切换上下文会让搜索时间翻倍。4.2 玩家走法校验、悔棋栈与难度三档参数玩家操作棋子的正确流程不是「点了就动」而是「先校验、再落子、最后切换走棋方」。程序要把玩家点击的起点终点拿去和当前方的合法走法列表比对匹配不上就原样不动匹配上了才MakeMove。很多半成品程序直接允许拖动棋子更新坐标结果出现「车穿子」「马跳过河」这些荒谬局面。Private Function TryPlayerMove(ByVal fromIdx As Integer, ByVal toIdx As Integer) As Boolean Dim i As Integer GenerateMoves Side, mvList For i 0 To mvList.Count - 1 If MoveFrom(mvList(i)) fromIdx And MoveTo(mvList(i)) toIdx Then PushHistory fromIdx, toIdx, Board(toIdx) 入悔棋栈 MakeMove mvList(i) Side -Side TryPlayerMove True Exit Function End If Next i TryPlayerMove False End Function悔棋栈的入栈信息至少要记录起点、终点、被吃掉的子三个字段。UnmakeMove恢复时把被吃子放回终点、起点还原成原棋子走棋方翻转回去。如果棋盘上同一位置出现红黑双方棋子重叠基本可以断定悔棋恢复顺序写错了。另外界面层最好再加一层「当前方」判断玩家在 AI 思考期间点击棋盘不应该产生任何动作。难度调节不要直接暴露给用户 Alpha-Beta 的alpha/beta窗口数那个纯属内部参数。最常见的做法是提供三档预设内部映射到最大深度和思考时间的组合难度档最大深度单步思考时间适用场景入门4 层500 毫秒新手练习走棋快标准5 层1500 毫秒有一定基础的爱好者高棋力6 到 7 层3000 毫秒体验「棋力惊人」深度和思考时间要联动单独把深度调到 7 却只给 800 毫秒程序往往搜不满 7 层就被时间预算打断返回的是 5 层的结果。这也是用户常反馈「深度改高了没变强」的原因。我一般把时间优先作为默认策略maxDepth设一个上限maxTimeMs作为硬预算到点就停。5. 常见问题与踩坑记录棋力惊人前的 4 个坑从「能走棋」到「棋力惊人」之间的路全是坑。这里整理 4 个最典型的问题全部按现象、原因、解决三个步骤记录后面再做同类引擎可以直接对照排查。5.1 复盘发现 AI 一直将军不进攻长将刷和现象AI 子力优势明显扫描棋谱却发现它反复将军把局面拖回重复不往前走杀棋。原因搜索里没有重复局面检测评估函数对将帅被将的惩罚权重不足AI 认为重复将军是最优解。解决给搜索加一个局面指纹计数检测到同一局面即将出现第三次时把这条分支的分值压低迫使引擎换走法。Private Function BoardKey() As String Dim s As String, i As Integer For i 0 To TOTAL - 1 s s Chr(Board(i) 1) Next i BoardKey s : CStr(Side) End FunctionBoardKey生成当前局面的唯一字符串每一次MakeMove前查字典判断是否已出现过两次若是则把该走法标记为禁着。这段代码在搜索前调用开销不大但能彻底避免 AI 输出长将走法。5.2 界面假死AI 思考时窗口转圈几十秒现象点击「电脑走棋」后整个窗口失去响应标题栏出现「未响应」直到搜索结束才恢复。原因Alpha-Beta 搜索是 CPU 密集任务在 VB6 单线程模型里会独占消息循环Timer 和界面重绘事件全被阻塞。解决在SearchRoot的深度循环里每隔一层调用DoEvents让消息循环有窗口呼吸。注意只放在层间不放内层另外把思考时间预算控制在 3 秒以内超过 5 秒的长时间搜索几乎必然被用户误认为死机。5.3 杀棋看不见离得越近反而越贪吃现象残局里 AI 放着一步将军连杀不走去贪吃边路小兵。原因固定深度搜索遇到水平线效应杀棋步骤恰好跨过了depth 0的边界搜索在将军之前就截断了。解决引入静态搜索Quiescence让到达叶子的吃子序列继续展开到底。这一步就是 3.3 里给的Quiescence函数它是水平线效应的「后悔药」。配合使用后连杀棋型的识别率会明显提高。5.4 抄来的评估参数是玄学棋力时强时弱现象从网上找了评估分值表替换进引擎后棋力不稳定时强时弱。原因评估函数里子力分、位置分、将军威胁几项是耦合的只改子力分不改位置表马的行动价值会被错误放大。解决先用经典比例把子力分对齐——车 1000、马 460、炮 450、相仕各 200、兵 100 过河加成——再跑自对弈调参。具体做法是让引擎和它自己对弈 20 局每局只调整一个参数增减 10%看胜率是否变化。胜率没变化说明该参数在当前搜索深度下不敏感没必要继续调胜率大幅波动才值得细致优化。6. 棋力验证三板斧自对弈、必杀局面与读秒测试引擎做出来之后怎么知道自己这套东西真的「棋力惊人」我的习惯是把验证拆成三个可重复的测试自对弈、必杀局面、读秒深度。这么做的原因是单看某一局没意义局部运气成分太大必须用统计和对照才能定位瓶颈。第一板斧是自对弈。步骤很简单第一种模式让引擎用不同深度互下比如深度 4 对深度 6第二种模式让同一个引擎分别用 500 毫秒、1500 毫秒、3000 毫秒思考时间对下。各跑 50 到 100 局记录总体胜率。如果深度 6 对深度 5 的胜率只有 55%大概率是评估函数存在系统性盲区比如把车马炮的分数配错了如果胜率明显拉开到 70% 以上说明搜索深度是当前主要瓶颈。这个实验也能帮你决定到底该调参数还是加时间。第二板斧是必杀局面测试。找一个明确的最优杀棋定式摆进棋盘比如车兵方对单缺象给引擎 3 秒思考时间看它能不能在限定的 12 步内走出杀棋。然后把这个定式里的静态搜索临时关掉再跑一遍对比明显的话你就能直观感受到第 3 章那套 Quiescence 到底值多少钱。这一步要特别留意引擎把卒升变和将帅照面这些规则处理对没有必杀定式最容易触发边角规则。第三板斧是读秒深度测试。写一个小工具记录同一个中局局面在 1 秒、2 秒、3 秒预算下的实际搜索深度和总节点数。如果 3 秒和 2 秒的结果完全一样而且深度没有递增那大概率是时间控制逻辑写错了——迭代加深还没到下一层就被时间检查强制退出而不是搜索真的到了瓶颈。深度稳步增加才说明迭代加深加得对时间预算用在了刀刃上。这个方向最大的教训是棋力惊人不是靠反复调参调出来的而是把走法生成、合法性检查、排序、剪枝、静态搜索整个骨架做对之后参数才配得上「调整」两个字。骨架歪了调参就是玄学骨架正了堆深度就是胜利。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑