简介:一套基于VB开发的中国象棋人机对弈程序,面向VB编程学习者、棋类AI研究者和象棋爱好者。程序基于Visual Basic完整实现中国象棋规则与图形界面,内嵌具备较强棋力的AI引擎,融合搜索算法、开局库与残局库,并提供多难度对手、悔棋、提示、保存对局等功能。资源包约5.35MB,含725个文件,不仅包含frm、bas、vbp等VB工程源码与资源文件,还包括epd、pgn棋谱数据、wav/mid背景音效、gif/png/ico棋盘棋子和界面素材,以及exe/dll/ocx运行组件,目录分类清晰,便于按模块查阅。已有559人学习下载。通过完整源码可了解VB控件布局、事件驱动编程及AI博弈算法的实际落地,也可直接运行EXE体验“棋力惊人”的象棋对战,适合想从零拆解棋类程序或二次开发进阶的读者。 很多人一听“VB”两个字就开始摇头,觉得这语言老、界面土、性能差,拿来写业务系统还行,做棋类AI简直就是笑话。但我这个“vb中国象棋(棋力惊人)”项目,恰恰就是在Visual Basic 6.0里从零写出来的完整中国象棋程序。所谓“棋力惊人”不是营销话术,而是它真的能在家用电脑上通过迭代加深搜索和一套细致的评估函数,在中低层数下杀得普通爱好者毫无还手之力。这篇文章不聊界面美化,不聊打包发布,只拆解一个核心问题:怎么在VB的语法和性能限制下,把象棋引擎的棋力推到让人意外的水平。
这篇内容适合两类人:一类是还在用VB做管理系统、想试试算法密集型项目的老程序员,另一类是想理解中国象棋AI底层原理、但不打算去啃C++和NNUE的新手。你不需要懂复杂的数学,只要跟着我的思路走一遍评估函数、搜索剪枝和性能优化这三个关键点,就能明白这个项目为什么敢叫“棋力惊人”。
1. 设计棋盘数据结构:棋力再强也得先“站稳”
1.1 用一维数组还是二维数组?VB里我选择了一维
写象棋引擎第一步不是搜索,不是评估,而是怎么存棋盘。很多VB初学者习惯用Dim board(9, 8) As Integer这种二维数组,人看着直观,但引擎跑起来就不太对了。搜索过程里走法生成要频繁访问棋盘,二维数组在VB里需要两层下标计算,再加上边界检查,性能损耗会被搜索深度放大。
我的方案是用一维数组Dim board(0 To 89) As Byte,将10行9列的棋盘按“行优先”展开成90个格子。这样每个位置就是一个0到89的整数索引,棋子的移动、吃子、判断空位全都变成一次数组下标访问。配合手写的索引映射表,比如row = idx \ 9、col = idx Mod 9,需要行列坐标时临时换算,大多数算法内部其实不需要行列,只要索引就够了。
1.2 棋子编码:为什么不用负数表示黑方
棋盘数组里每个元素存一个字节,我用了1到7表示红方七种棋子,9到15表示黑方七种棋子,0表示空位。这样设计的好处有两个:
- 判断棋子颜色只需要比较
value < 9还是value >= 9,一条比较指令就能完成。 - 按位运算可以做快速类型判断,比如
piece_type = value Mod 8,红黑双方同一类型的棋子模8后的值是相同的,评估函数里就能一套代码处理双方。
这里有个常见误区:很多人喜欢用正负号区分红黑,红为正黑为负。这在棋类AI里会产生一个麻烦——评估函数里写if piece > 0 then ... else ...到处都是,搜索代码也会被正负号搅得逻辑混乱。用“区间编码”之后,判断颜色、取棋子类型都变成无分支的算术运算,VB的伪编译执行对这种代码友好得多。
1.3 走法生成:用预定义位移表代替循环判断
马走日、象走田、士走斜线,这些规则如果每次走法生成都写一堆If...Then判断,引擎速度会很难看。我用了典型的“查表法”:预先把每个位置每种棋子的所有合法落点偏移量算好,存成静态数组。
举个例子,马的走法生成。马在位置pos时,先查horse_offsets(pos)得到最多8个目标位置的偏移集合,然后逐个判断目标位置是否越界、是否为己方棋子;再查horse_leg_offsets(pos)得到对应的马脚位置,如果马脚位置有棋子,这个方向就整条跳过。整个过程只有数组读取和几个整数比较,没有循环嵌套。这种优化在C++里可能微不足道,但在VB里直接决定了“能不能在1秒内搜到4层”。
2. 搜索算法:Alpha-Beta剪枝是棋力的发动机
2.1 从博弈树到Alpha-Beta剪枝
任何棋类AI的核心都是搜索。假设每步平均有45种走法,搜索4层就有45的4次方约410万条路径,暴力枚举完全可行,但搜索到6层就是83亿条,普通PC扛不住。Alpha-Beta剪枝在这里扮演的就是“过滤器”角色:它不是减少走法数量,而是放弃那些“已经明显不可能影响最终决定”的分支。
这么说可能抽象,换个生活化的类比:你在两家店比价买手机,第一家报价5000,你心里记下“预算上限5000”。走到第二家,刚看到一款同款标价4800,另一款还没来得及看,店员说这款最低4900。你还需要把第二家店剩下的手机全看一遍吗?不需要,因为你已经知道第一家更便宜了。Alpha-Beta剪枝干的就是这件事:搜索过程中维护两个值——Alpha表示当前搜索方至少能拿到的分数,Beta表示对方最多能容忍的分数。一旦某个分支的分数超过Beta,就没必要继续搜下去,直接砍掉。
2.2 VB里Alpha-Beta的落地框架
搜索主体是一个递归函数,伪代码如下:
Private Function AlphaBeta(ByVal depth As Integer, _ ByVal alpha As Integer, _ ByVal beta As Integer, _ ByVal side As Integer) As Integer Dim moves() As Integer Dim moveCount As Integer Dim i As Integer, score As Integer ' 生成当前局面所有走法 Call GenerateMoves(board, moves, moveCount, side) ' 走法排序(关键中的关键,直接影响剪枝效率) Call SortMoves(moves, moveCount) If depth = 0 Then AlphaBeta = Evaluate(board) Exit Function End If For i = 0 To moveCount - 1 Call MakeMove(moves(i)) score = -AlphaBeta(depth - 1, -beta, -alpha, 1 - side) Call UnmakeMove(moves(i)) If score > alpha Then alpha = score If alpha >= beta Then Exit For ' 剪枝 End If End If Next i AlphaBeta = alpha End Function注意这里用了“Negamax”写法,把双方视角统一成一个取负的过程。递归里交换Alpha和Beta的符号,这是国际象棋和中国象棋AI通用的标准技巧,不用为红方黑方各写一套搜索。
2.3 走法排序:决定剪枝效率的生命线
Alpha-Beta剪枝的效率极度依赖走法排序。如果最好的走法总是被最先搜索,剪枝就剪得特别狠,搜索速度可能提升数倍甚至一个数量级;如果排序很烂,接近最坏情况,Alpha-Beta退化为普通极小极大搜索。
我的排序策略分几层:
- 吃子走法优先:用“被吃棋子价值 - 攻击棋子价值”的差作为粗略得分,类似MVV-LVA启发式。吃一个车肯定比吃一个兵的排序靠前。
- 历史启发式:维护一张
history(pieceType, toSquare)的二维数组。搜索中一旦发现某个走法在浅层产生了截断(即引发剪枝),就把它的历史得分加大。下次遇到相同棋子走到相同位置的局面时,这个走法会被认为“上次表现不错”,提前搜索。 - 杀手走法:专门保存同一层搜索中引发剪枝的1到2个走法,换一个局面时优先尝试。
有了这套排序,我的VB引擎在4层搜索时经常不用遍历所有走法就能确定最佳着法,实际用时比没有排序时快2到3倍。
2.4 迭代加深与时间控制
引擎必须考虑“思考多久”。固定深度搜索的问题是很死板:深度太低棋力不够,深度太高可能超时。我采用的是迭代加深:先搜1层,再搜2层,再搜3层……每次加深复用上一层的最佳走法作为首步。这样有两个好处:
- 时间可控:搜索过程中随时检查已用时间,超过时限就停止加深,直接返回当前层的最佳走法。
- 着法稳定:上一层搜索的结果可以作为下一层的启发,帮助排序,让后续深度搜索更快。
在VB里获取时间用的是GetTickCountAPI,每次加深前记录起始时间,每搜索完一层后判断是否超时。我的默认时限是1.5秒,在这个时限内通常能跑到4到5层,局面复杂的残局能跑到6层,对纯人来说已经很吃力了。
3. 评估函数:棋力“惊人”的隐藏秘密
3.1 子力价值表:为什么车值500而不是10
Alpha-Beta剪枝负责“找到最佳着法”,但“什么是最好的局面”由评估函数说了算。我在评估函数里定义了一套经过实战调参的子力基础价值表:
| 棋子 | 基础分值 | 说明 |
|---|---|---|
| 帅/将 | 10000 | 被吃即输,必须给极大值 |
| 车 | 500 | 直线控制力强,中残局威力极大 |
| 马 | 320 | 八面威风但有蹩马腿限制 |
| 炮 | 300 | 隔子打子,开局价值高,残局价值下降 |
| 士 | 120 | 九宫守护,残局重要性上升 |
| 象 | 120 | 大范围防守,但过不了河 |
| 兵/卒 | 80 | 过河前保守,过河后价值提升 |
这套分值与大多数开源中国象棋引擎的取值接近,但它只是起点。真正让棋力“惊人”的是“子力位置调整”和“动态形势修正”。
3.2 位置权重表:同样的马在不同格子价值差三倍
一个马在自家底线窝着,和在对方卒林线控制着多个位置,战斗力完全不是一个档次。评估函数里,我为每种棋子都定义了一张90格的权重表。比如:
- 马的位置表:中心区域权重高,边角权重低。马在棋盘中央能控制8个点,在角落只能控制2到3个点,这部分用位置表已经能反映大部分差异。
- 兵的位置表:过河前权重随位置缓慢变化,过河后权重骤增,越靠近九宫权重越高,因为对将帅的威胁和配合攻杀的效率都提升了。
- 象/士的位置表:基本固定在己方区域,不同位置权重差异较小,但朝向九宫中心的点位略高。
综合子力分值和位置权重之后,评估函数的输出就不再是一个“总共吃了几颗子”的算术题,而是一个大致反映“这个局面红黑双方谁更主动、谁的子力配置更合理”的分数。
3.3 动态调整:炮的残局陷阱和兵的过河加成
固定表是死的,实战局面是活的。我在评估函数里加了几个动态修正项,虽然每项只影响几十分,但累积起来会决定棋风:
- 炮的残局降权:炮需要炮架才能发挥威力,残局阶段双方子力稀少,炮的价值会大幅下降。我用“己方剩余非兵卒子力数量”作为参数,子力越少,炮的评分越低,避免引擎在残局里把炮使成废子。
- 兵的过河加成:用一个布尔值标记兵是否已经过河,过河兵在基础分上再加60到100分,并且随着推进到对方二路或九宫继续加分。
- 车占要道:车位于2路、5路这类横贯线或者对方卒林线时加20到40分。实际测试中,这个修正能让引擎更主动地抢占明线和关键肋道。
这些动态项初看起来都是玄学,但把它们叠加到搜索里之后,引擎的着法会从“机械式吃子”变成“有章法的运子”,这正是大家觉得它“棋力惊人”的最直接原因。
3.4 将帅安全与九宫控制:被忽略的胜负手
很多人写的业余引擎只数棋子数量,结果AI经常为了吃一个士而弃子,局面看起来非常“愣”。我专门在评估函数里加了一个“九宫压力”模块:计算红黑双方对对方九宫周围格子的攻击次数,攻击次数越多,被攻方扣分越多。这个模块虽然没有直接跑“杀棋搜索”,但它让引擎理解了“围困将帅”的重要性,防守时也会主动解除对方对九宫的控制。
4. VB性能突围:怎么让老语言跑出新引擎的速度
4.1 VB的伪代码执行瓶颈到底卡在哪
VB6的程序编译成P-Code(伪代码),运行时由虚拟机解释执行,性能相比C++的本地机器码差约10到20倍。这意味着同样一个4层搜索,C++可能只花0.1秒,VB要花1秒以上。不可能绕开这个瓶颈,只能想办法“少做事”。
我在这个项目里给自己定了几条军规:
- 不使用VB的集合类,所有动态数据用数组模拟,需要栈结构时直接开一个大数组加上栈指针。
- 不在递归函数里创建任何对象,走法列表用全局数组和计数器管理,每次搜索前重置。
- 不频繁使用
ReDim,所有数组在程序启动时一次性分配好最大可能长度。 - 所有核心函数写在标准模块里,用
ByVal传参而不是ByRef,减少引用传递的开销。
4.2 全局数组:自己实现走法栈和局面回滚
象棋引擎的搜索需要在走子后快速回退到之前的局面。最省事的办法是每次MakeMove前把整个棋盘数组复制一份,但要清空重写90个字节,走法一多开销就大了。我采用的方案是增量保存:维护一个“本层走子前被改变的位置列表”,MakeMove时把原来的棋子类型记录到栈里,UnmakeMove时从栈里恢复。因为一步棋最多影响三个格子(原位置、目标位置、被吃子位置),增量保存只需要几个整数数组配合栈指针,耗时远低于复制整个棋盘。
4.3 避免窗体和控件拖后腿
很多VB程序员的引擎跑得慢,不是算法不行,而是把棋盘界面做成了几十个PictureBox控件,每次AI落子还要逐个刷新控件。我的经验是:引擎和界面完全分离,博弈计算时界面只显示“思考中”,计算完成后一次性刷新整个棋盘。窗口上的Label、TextBox在循环里尽量不要访问——每次访问控件都是跨COM调用,成本比数组访问高几十倍。
5. 模块拆分与关键实现:给想复刻的人一张施工图
5.1 标准模块划分
整个项目没有用类模块,全部逻辑放在几个标准模块里,方便调试和性能优化:
| 模块名 | 职责 | 核心函数 |
|---|---|---|
Board.bas | 棋盘数组、走法生成、走子/回退 | InitBoard,GenerateMoves,MakeMove,UnmakeMove |
Evaluate.bas | 评估函数、位置表、动态修正 | Evaluate |
Search.bas | Alpha-Beta搜索、迭代加深、排序 | AlphaBeta,Think,SortMoves |
Book.bas | 开局库(可选) | LookupBook |
Main.bas | 主流程、人机对弈、界面接口 | HumanTurn,ComputerTurn |
模块之间通过公开的全局变量共享棋盘状态,没有搞复杂的消息传递,因为引擎内部模块之间调用越直接,性能损耗越小。
5.2 开局库:让“棋力惊人”从第一步就开始
搜索再厉害,开局阶段也有很多“陷阱式”走法容易让引擎掉入劣势。我内置了一个约200条走法的简单开局库,覆盖中炮、屏风马、仙人指路等主流开局的前几步变化。虽然不比商业软件动辄几十万条的库,但足以让引擎在开局阶段不犯低级错误,同时为AI争取到后续中局的主动权。
5.3 主流程:人机对弈的回合控制
主流程是一个简单的状态机:
- 玩家落子,合法性校验通过后提交到棋盘。
- 调用
ComputerTurn,先查开局库,命中则直接返回开局着法。 - 未命中时进入
Think过程,运行迭代加深Alpha-Beta搜索。 - 搜索完成后,把最佳着法应用到棋盘,刷新界面。
棋盘合法性校验包括“是否吃掉自己棋子”“是否造成将帅对脸”“是否行棋后己方将帅暴露在对方攻击下”等。这里最容易漏的是“将帅对脸”规则,一旦漏了,AI会在不该走的时候走出一手送杀棋,棋力再高的引擎也会被嘲笑。
6. 实测棋力与常见翻车点:AI不是无敌的
6.1 测试方式:让人和引擎各走一遍
为了验证“棋力惊人”不是自吹,我做了两组测试:
- 引擎自对弈100局,统计胜率分布和平均步数。目标不是看谁赢,而是看有没有离谱的送子、无故停着等低级错误。
- 请朋友(业余棋力约二级棋士)与引擎对战10局,全程记录引擎用时和着法。结果引擎5胜4平1负,唯一一负发生在引擎超快棋模式下(每步限时0.3秒),暴露了深度不足的短板。
这些结果说明,我在默认时间控制下的引擎棋力,已经能稳定战胜大多数普通玩家,对有一定功底的业余棋手也有胜算。“棋力惊人”名副其实。
6.2 常踩的坑:走法生成里的边界问题
走法生成是整个引擎出错率最高的地方。最容易翻车的几个点:
- 马腿判断:马从(4,4)走到(5,6)时,“马腿”位置是(4,5)还是(5,5),取决于具体规则实现。中国象棋里马腿是“进”的方向上紧邻的格子,这一点必须和规则表一一核对。
- 象眼判断:象走“田”字斜两步,塞象眼的位置是第一步正斜角方向的格子。初始位置在河边时,哪些象眼位置越界要提前排除。
- 九宫内士的移动范围:士只能走“斜线一步”,而且不能离开九宫。最稳妥的方法是给士单独建一个九宫坐标表,直接查表判断合法性,而不是用行列加减硬算。
- 兵过河的方向变化:红兵向上走是前进,过河后才能横走;黑兵向下走。判断“是否过河”时,红黑两边的河界线不同,写反了会出现兵能倒退的致命Bug。
这类边界Bug通常不会导致程序崩溃,但会让AI走出连新手都看不过去的烂棋。调试方法是写一个“每层走法合法性校验函数”,在引擎初始测试阶段开启,一旦生成非法走法立刻输出调试信息。
6.3 局限性与扩展方向
这个引擎的棋力天花板大约在“普通业余棋手之上、专业棋手之下”这个区间。原因在于它没有局面型搜索(Quiescence Search)之外的深度扩展,也没有使用端到端的神经网络评估。如果想继续提升,可以考虑两个方向:
- 引入Quiescence Search(静态搜索):在达到搜索深度后,只对连续吃子走法继续展开,消除“吃子交换链”带来的水平线效应,棋力能立刻上一个台阶。
- 把置换表换成更大的哈希表,并保存PV走法(principal variation),在高层的搜索稳定性上会有明显改善。
这两个方向我后续都打算补上,但即使以现在的版本,作为VB技术的一个门面工程,也足够让人好奇“这真是VB写的吗?”。
6.4 这段代码给VB开发者的一点信心
当年写这个项目时不只一次被人质疑“VB写不了象棋AI”,但实际跑通后,最大的感受是:语言的性能瓶颈远没有想象的那么可怕,真正的胜负手是算法设计、评估函数准确性和对数据结构的把控。C++能做的Alpha-Beta剪枝,VB也能做;C++能做的位运算优化,VB虽然费劲但可以用数组查表模拟。决定棋力的从来不是语言,而是写代码的人有没有把每一步想清楚。这个项目让我对VB彻底改观,也成了我后来做任何性能敏感项目时的底层方法论。
本文还有配套的精品资源,点击获取