
简介这是一份面向象棋游戏开发爱好者与编程学习者的开源象棋引擎代码核心为公开源码的计算机象棋程序可模拟对弈逻辑并达到一定棋力水平适合初学者研究完整引擎架构也便于进阶开发者借鉴搜索与评估策略。压缩包共37个文件约56KB以16个h头文件、4个cpp源文件为主辅以txt说明、bas与frm界面模块、rc资源脚本及ico图标等头文件分别承载搜索算法、棋步生成、棋盘结构与开局预生成等核心定义源文件则实现引擎主逻辑与输出重定向并包含UCI标准接口便于对接图形界面。目前已有790人学习下载。通过研读这套代码读者可掌握棋局评估、Alpha-Beta剪枝、位棋盘表示等关键实现理解从用户交互到搜索决策的完整链路为自研象棋程序提供可复用的参考基础。1. 好的开源代码象棋引擎从零跑通一个能下赢你的对手你打开 GitHub 搜“chess engine”跳出来几百个仓库star 从个位数到几千都有。随手 clone 一个make一下跑起来发现它连三步杀都看不出来再换一个编译都过不了。这不是你运气差而是“好的开源代码象棋引擎”这个筛选条件本身就模糊——什么叫好棋力高、代码干净、文档全、能嵌入到自己的项目里这四个维度往往互相打架。我做过一个需要把象棋引擎嵌进教学工具的项目前后试了七八个开源实现最后稳定用的是 Stockfish 的裁剪版加一个轻量级 UCI 封装。这篇笔记就按我实际踩过的路把“选哪个、怎么编译、参数怎么调、棋力怎么验证、坑在哪”一次讲清楚。适合两类人想拿现成引擎做二次开发的工程师以及想读源码学搜索算法的开发者。下面所有操作都在 Linux 和 macOS 上验证过Windows 用 WSL 或 MSYS2 也能跟。2. 选型为什么 Stockfish 仍然是默认答案以及什么时候不该用它2.1 开源象棋引擎的三个梯队把市面上活跃的开源象棋引擎按棋力和维护状态分一下大致是这么个格局。第一梯队是 Stockfish常年霸占各类引擎排行榜首位代码用 C17 写搜索和评估分离得很干净UCI 协议支持完整社区活跃到每周都有 PR。第二梯队是 Leela Chess Zero走的是神经网络路线棋力接近 Stockfish 但硬件门槛高没有像样的 GPU 基本跑不动而且模型文件动辄几百 MB。第三梯队是一堆教学向或实验向的引擎比如 Giraffe、Sunfish 这类代码短小精悍适合读算法但棋力大概在业余初段到业余三段之间拿来做产品不够用。如果你要的是“能下赢大部分人类”的引擎Stockfish 是默认答案。但如果你要的是“能塞进嵌入式设备”或者“代码量控制在两千行以内方便自己改”那 Stockfish 就太重了。我见过有人把 Stockfish 往 STM32 上搬最后砍到只剩 alpha-beta 搜索加简单子力评估棋力掉到业余入门水平但确实跑起来了。所以选型的第一步不是看 star 数而是先问自己目标平台的算力预算和内存预算分别是多少。2.2 编译 Stockfish 的最小命令与参数含义假设你已经决定用 Stockfish从源码到可执行文件其实就三步。先拿源码再编译最后验证。# 克隆仓库这里用官方镜像地址的通用写法实际以你本地能访问的源为准 git clone stockfish-repo-url cd stockfish/src # 编译默认会针对当前 CPU 做指令集优化 make -j$(nproc) build ARCHx86-64-modern # 验证跑一个内置的基准测试看每秒搜索节点数 ./stockfish benchmake那行里的ARCH参数是关键。x86-64-modern会启用 BMI2、AVX2 等指令集搜索速度比x86-64通用版快 30% 到 50%。但如果你编译出来的二进制要分发到老机器上就得用x86-64甚至x86-64-old否则会直接非法指令崩溃。-j$(nproc)是并行编译核多就快但内存小于 4G 的机器建议改成-j2不然链接阶段容易 OOM。bench命令跑完会输出一个Nodes/second数值这个数就是你的硬件上引擎的搜索吞吐量后面调参数时要以它为基准。2.3 什么时候该放弃 Stockfish 转向轻量引擎有三种情况我会建议你换掉 Stockfish。第一种是目标设备内存小于 64MBStockfish 的哈希表默认就要占几十 MB砍到最小也会影响棋力。第二种是你要把引擎嵌进一个已经用其他语言写的项目里比如 Python 或 JavaScript这时候用 Stockfish 的 C 二进制通过子进程通信虽然可行但部署复杂度高不如找一个同语言的轻量实现直接调函数。第三种是你主要目的是教学想让学员在一周内读懂全部代码那 Stockfish 的代码量会直接劝退。轻量引擎里我比较推荐的是 Sunfish纯 Python 写的核心搜索不到 200 行棋力大概业余 1 级但用来讲 alpha-beta 剪枝和局面评估足够了。3. 把引擎跑起来UCI 协议对接与第一个可交互对局3.1 UCI 协议的最小交互流程开源象棋引擎绝大多数都实现了 UCI 协议这是一个基于文本行的标准接口。你不需要读引擎源码只要会发命令和读输出就能控制它。最小交互流程是这样的启动引擎进程后先发uci引擎会返回一堆id和option行最后以uciok结束。然后发isready等readyok。接着用position startpos moves e2e4 e7e5设置局面用go movetime 1000让引擎思考一秒引擎会输出bestmove加具体着法。整个过程就是纯文本往返用任何语言都能实现。import subprocess # 启动引擎进程路径换成你编译出来的可执行文件 engine subprocess.Popen( [./stockfish], stdinsubprocess.PIPE, stdoutsubprocess.PIPE, textTrue, bufsize1 ) def send(cmd): engine.stdin.write(cmd \n) engine.stdin.flush() def read_until(token): lines [] while True: line engine.stdout.readline().strip() lines.append(line) if line.startswith(token): return lines # 握手 send(uci) read_until(uciok) send(isready) read_until(readyok) # 设置局面并让引擎走一步 send(position startpos moves e2e4 e7e5) send(go movetime 1000) result read_until(bestmove) print(result[-1]) # 输出形如 bestmove g1f3 ponder ...这段代码里bufsize1和textTrue是必须的否则 Python 的缓冲会导致读不到实时输出。read_until里用startswith而不是是因为引擎返回的行可能带额外空格或参数。go movetime 1000表示给引擎 1000 毫秒思考时间时间一到必须返回着法。如果你想要固定深度而不是固定时间可以换成go depth 10但深度和耗时不是线性关系深度每加一层耗时可能翻好几倍。3.2 用 Python 封装一个可对战的命令行棋手有了上面的基础封装一个能和你下棋的命令行程序就不难了。核心逻辑是维护一个着法列表每次你走一步就追加到列表里然后让引擎也走一步再追加。棋盘显示可以用简单的字符画不需要图形界面。def board_to_text(moves): # 这里用一个极简的棋盘表示实际项目建议用 python-chess 库 # 仅示意流程不处理吃子和合法性校验 board [ [r,n,b,q,k,b,n,r], [p,p,p,p,p,p,p,p], [ , , , , , , , ], [ , , , , , , , ], [ , , , , , , , ], [ , , , , , , , ], [P,P,P,P,P,P,P,P], [R,N,B,Q,K,B,N,R], ] return \n.join( .join(row) for row in board) moves [] while True: print(board_to_text(moves)) user_move input(你的着法如 e2e4输入 q 退出) if user_move q: break moves.append(user_move) send(position startpos moves .join(moves)) send(go movetime 1000) best read_until(bestmove)[-1].split()[1] print(f引擎走{best}) moves.append(best)这段代码省略了着法合法性校验和棋盘更新实际用的时候建议直接上python-chess库它能把 UCI 着法转成棋盘对象还能判断将杀和逼和。go movetime 1000对新手来说已经足够强了如果你想让它更弱一点可以改成go depth 1但即使深度 1Stockfish 也能秒杀大部分没学过棋的人。想让它故意走差棋得用Skill Level这个 UCI 选项下一节会讲。3.3 引擎选项的读取与设置UCI 协议里引擎会通过option行声明自己支持哪些可调参数。Stockfish 常见的选项包括Threads搜索线程数、Hash哈希表大小单位 MB、Skill Level棋力等级0 到 20、MultiPV同时输出多个候选着法。设置格式是setoption name 选项名 value 值注意name和value是关键字选项名里有空格也要原样写。# 把引擎棋力降到 5 级哈希表设为 64MB单线程 send(setoption name Skill Level value 5) send(setoption name Hash value 64) send(setoption name Threads value 1) send(isready) read_until(readyok)Skill Level设为 0 时引擎几乎随机走棋设为 20 是全力。我实测下来设为 5 大概相当于业余 3 级设为 10 大概业余 1 段。Hash设太小会影响中局搜索深度设太大在低内存设备上会拖慢启动。Threads不是越多越好超过物理核数反而因为调度开销导致搜索速度下降。这些参数改完必须发isready并等到readyok才生效直接发go可能会被忽略。4. 避坑编译、对接和棋力验证中最容易翻车的五个地方4.1 编译报错 “illegal instruction” 或直接崩溃现象是编译成功但一运行就提示非法指令或者跑bench到一半突然挂掉。原因几乎都是ARCH参数选得太激进编译时用了当前 CPU 支持但目标机器不支持的指令集。比如你在支持 AVX2 的机器上编译了x86-64-modern拿到只支持 SSE4 的老机器上跑第一条 AVX2 指令就会触发异常。解决办法是编译前先确认目标机器的 CPU 指令集用cat /proc/cpuinfo | grep flags看有没有avx2和bmi2。如果没有就老老实实用ARCHx86-64。如果是要分发给不确定的机器用x86-64-old最保险代价是搜索速度慢一些。4.2 引擎进程卡死readline 一直阻塞现象是发了go之后Python 的readline永远等不到bestmove。原因通常是引擎在思考时输出了大量info行而你的读取逻辑只读固定行数就停了导致管道缓冲区被填满引擎写不进去就卡住。解决办法是把读取逻辑改成循环读到bestmove为止并且不要在中间做耗时操作。另一个可能原因是bufsize设成了默认值Python 在缓冲模式下会等缓冲区满才返回改成bufsize1并加textTrue就能实时读取。如果还是卡检查引擎进程是不是已经因为非法着法崩溃了可以在read_until里加超时。4.3 棋力验证时用错基准误判引擎强弱现象是你觉得某个引擎很弱因为它在你写的测试局面里走了几步臭棋。原因很可能是你给的测试局面本身就不公平比如让引擎在少一个车的局面下走棋它怎么走都是输。验证引擎棋力要用标准开局和标准测试集比如bench命令自带的局面集或者用go depth 10在初始局面下看它选什么着法。更严谨的做法是让两个引擎互相对弈若干局用 Elo 计算工具算分差。我一般会跑 100 局快棋每步 100ms然后看胜率。如果两个引擎胜率在 45% 到 55% 之间说明棋力接近如果一边倒那差距至少在 100 Elo 以上。4.4 哈希表设置过大导致启动失败现象是setoption name Hash value 2048之后引擎没反应或者直接退出。原因是 32 位编译的引擎最大只能寻址 4GB 内存设 2048MB 哈希表加上其他开销很容易超过限制。解决办法是确认你的引擎是 64 位编译的用file ./stockfish看输出里有没有64-bit。如果是 32 位哈希表最大设 1024 甚至 512。另外即使 64 位系统如果物理内存只有 2GB设 2048MB 哈希表也会导致频繁换页搜索速度反而下降。哈希表大小一般设为可用物理内存的 1/4 到 1/2 比较合适。4.5 多线程搜索反而变慢现象是Threads从 1 调到 4 之后bench的Nodes/second不升反降。原因是 Stockfish 的多线程搜索有同步开销线程数超过物理核数时操作系统调度会让线程互相抢 CPU导致缓存命中率下降。解决办法是Threads不要超过物理核数超线程的核按物理核算。比如 4 核 8 线程的 CPU设 4 就行设 8 大概率变慢。另外Hash太小时多线程也会变慢因为多个线程抢同一个哈希表锁。如果一定要多线程哈希表至少设 256MB。5. 进阶用 MultiPV 做局面分析以及一个验证引擎是否“理解”棋局的技巧5.1 MultiPV 的用法与输出解读MultiPV是 Stockfish 里我最常用的分析功能。默认情况下引擎只输出它认为最好的那一步但设了MultiPV之后它会同时输出前 N 个候选着法及其评分。这对复盘和教学特别有用因为你能看到引擎认为“第二好”的着法差在哪里。send(setoption name MultiPV value 3) send(isready) read_until(readyok) send(position startpos moves e2e4 e7e5 g1f3) send(go depth 12) lines read_until(bestmove) for line in lines: if multipv in line and pv in line: print(line)输出里每一行multipv会带一个序号、一个cp或mate评分以及后续的pv着法序列。cp是厘兵分100 代表一个兵的优势。mate后面跟的数字表示多少步内将杀正数是我方将杀负数是被将杀。注意MultiPV设太大会拖慢搜索因为引擎要维护多个候选线的完整搜索一般设 3 到 5 就够了。设成 10 以上搜索深度会明显下降分析结果反而不准。5.2 用“让子测试”判断引擎是否真的理解局面有一个我常用的土办法验证引擎是不是在“背谱”而不是“理解”从初始局面开始手动拿掉引擎的一个车然后让它执先走。如果引擎真的理解局面它会表现得保守尽量简化局面争取和棋如果它只是背开局库拿掉车之后它会走一些看似正常但实际加速失败的着法。具体操作是发position startpos moves后面不跟任何着法然后用setoption name UCI_Chess960 value false确保标准规则再手动构造一个少车的 FEN 局面发给引擎。# 构造一个白方少一车的 FEN白方走棋 position fen rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/1NBQKBNR w KQkq - 0 1 go depth 15这个 FEN 里白方原本的 a1 车被拿掉了。观察引擎的输出评分如果评分在 -200 到 -400 之间说明它意识到自己劣势但还能下如果评分直接跳到 -800 以上说明它评估函数对子力差距很敏感。这个测试不能量化棋力但能快速判断引擎的评估函数是不是正常工作。我一般会在换引擎版本或者改编译参数之后跑一次确认没有把评估模块改坏。5.3 把引擎嵌进自己项目的两种集成方式最后说集成。如果你只是想在本地工具里调引擎用子进程加 UCI 文本通信是最省事的任何语言都能做缺点是每次调用要启动进程频繁调用时开销大。另一种方式是把引擎编译成动态库直接调函数速度快但接口不标准每个引擎的 API 都不一样换引擎就要重写对接层。我一般会先用子进程方式跑通功能等性能瓶颈出现了再考虑动态库。如果目标平台是移动端Stockfish 有 Android 和 iOS 的编译脚本但需要交叉编译工具链坑比较多建议直接找现成的封装库不要自己从源码编。我自己的习惯是每换一个引擎版本先跑bench记录节点数再跑 100 局快棋看胜率最后用让子测试确认评估函数没坏。这三步走完才敢把它放进产品里。希望帮到你。本文还有配套的精品资源点击获取