资讯动态

中国象棋C语言源码:从棋盘表示到Alpha-Beta搜索的完整实现

发布时间:2026/10/20 21:58:22 来源:尧图企业网站定制
简介这份资源是一套用C语言编写的中国象棋游戏完整源代码面向希望以棋类项目巩固C语言基础、锻炼逻辑思维的初学者与进阶开发者。代码围绕棋盘二维数组建模、各棋子走法规则、吃子与走法合法性判断、将军与将死等结束条件展开并实现命令行交互同时预留AI搜索、图形界面与棋谱记录回放等扩展方向。压缩包共85个文件约143KB以38个ico图标、14个h头文件、10个cpp源文件为主另含cur光标、txt说明、dsp与dsw工程文件及可执行程序工程结构完整可直接编译运行。目前已有997人学习下载。借助这套源码读者能对照实现细节理解数据结构与算法在棋类游戏中的落地方式并在此基础上尝试加入Minimax搜索或图形化改造是学习C语言项目实践的实用参考。1. 一份能直接编译运行的中国象棋 C 语言源码到底能拿来做什么很多开发者第一次接触棋类 AI都是从一份能跑起来的完整源码开始的。这份中国象棋 C 语言源代码核心价值不在于界面多华丽而在于它把棋盘表示、走法生成、局面评估、搜索算法这条完整链路用纯 C 写透了。你拿到手就能编译跑起来就能跟电脑对弈改几行参数就能调整 AI 强度。它适合两类人一类是刚学完 C 语言数据结构、想找个真实项目练手的开发者另一类是想搞懂棋类 AI 搜索机制、但不想被庞大工程劝退的熟手。整个项目没有第三方依赖标准 C 库就能编译这在今天动辄拉一堆包的环境里反而成了优点。2. 项目架构拆解棋盘、走法、评估、搜索四层怎么串起来2.1 棋盘表示与数据结构选型这类项目最常见也最稳妥的做法是用一个一维或二维整型数组存棋盘。一维数组的好处是索引计算简单index row * 9 col走法生成时偏移量直接加减不用处理二维数组的行列边界判断。棋子编码一般用正负号区分红黑绝对值表示兵种比如1是帅/将2是仕/士3是相/象4是马5是车6是炮7是兵/卒。这样判断敌我只需要看符号判断兵种只需要看绝对值代码里到处都是这种小技巧。// 棋盘定义10 行 9 列一维数组存储 #define BOARD_ROWS 10 #define BOARD_COLS 9 #define BOARD_SIZE (BOARD_ROWS * BOARD_COLS) // 棋子编码正数红方负数黑方绝对值表示兵种 // 1帅/将 2仕/士 3相/象 4马 5车 6炮 7兵/卒 int board[BOARD_SIZE]; // 索引转换行列转一维下标 static inline int pos(int row, int col) { return row * BOARD_COLS col; } // 判断某位置是否在棋盘内 static inline int in_board(int row, int col) { return row 0 row BOARD_ROWS col 0 col BOARD_COLS; }这段代码里pos和in_board是两个高频调用的内联函数走法生成时每走一步都要判断目标位置是否合法。用static inline是为了避免函数调用开销在搜索深度上去之后这点优化会累积成可观的性能差异。棋盘数组直接开全局省去传参这在棋类程序里是常见做法因为整个程序生命周期只有一个棋盘实例。2.2 走法生成每个兵种一套规则走法生成是整个项目里代码量最大、也最容易写错的部分。每个兵种都要单独处理而且红黑方向相反边界条件各不相同。常见做法是写一个generate_moves函数遍历棋盘上所有己方棋子对每个棋子调用对应的生成逻辑把合法走法存进一个走法数组。// 走法结构起点、终点、被吃棋子 typedef struct { int from; int to; int captured; } Move; // 马走法偏移8 个方向先判断蹩马腿 static const int horse_dr[8] {-2, -2, -1, -1, 1, 1, 2, 2}; static const int horse_dc[8] {-1, 1, -2, 2, -2, 2, -1, 1}; // 对应的马腿位置偏移 static const int leg_dr[8] {-1, -1, 0, 0, 0, 0, 1, 1}; static const int leg_dc[8] {0, 0, -1, 1, -1, 1, 0, 0}; void gen_horse_moves(int r, int c, int side, Move *moves, int *count) { for (int i 0; i 8; i) { int nr r horse_dr[i]; int nc c horse_dc[i]; if (!in_board(nr, nc)) continue; // 蹩马腿判断马腿位置有棋子则不能走 int lr r leg_dr[i]; int lc c leg_dc[i]; if (board[pos(lr, lc)] ! 0) continue; int target board[pos(nr, nc)]; // 目标位置不能是己方棋子 if (target * side 0) continue; moves[*count].from pos(r, c); moves[*count].to pos(nr, nc); moves[*count].captured target; (*count); } }马的走法有两个关键点一是八个方向的偏移量要跟马腿偏移量一一对应写反了就会出现“马穿墙”的玄学 bug二是蹩马腿判断必须在目标位置判断之前因为马腿被蹩时无论目标是什么都不能走。炮的走法更特殊需要单独处理“翻山”逻辑沿直线扫描遇到第一个棋子后继续找第二个棋子第二个如果是敌方才能吃。兵/卒过河前只能前进过河后可以左右移动这个状态判断依赖当前行号。2.3 局面评估函数子力价值加位置修正评估函数决定了 AI 的“棋风”。最简单的做法是只算子力价值车 9 分、马 4 分、炮 4.5 分、兵 1 分仕相 2 分。但这样 AI 会显得很笨因为它不知道棋子该往哪放。常见改进是加位置分表比如马在中心比在边线得分高兵过河后加分车占开放线加分。// 基础子力价值红方视角黑方取负 static const int piece_value[8] {0, 10000, 200, 200, 400, 900, 450, 100}; // 兵过河后的位置奖励越靠近对方底线越高 static const int pawn_bonus[BOARD_ROWS] { 0, 0, 0, 0, 10, 20, 30, 40, 50, 60 }; int evaluate() { int score 0; for (int i 0; i BOARD_SIZE; i) { int p board[i]; if (p 0) continue; int abs_p p 0 ? p : -p; int val piece_value[abs_p]; // 兵/卒过河奖励 if (abs_p 7) { int row i / BOARD_COLS; if (p 0 row 4) val pawn_bonus[row]; if (p 0 row 5) val pawn_bonus[9 - row]; } score (p 0 ? val : -val); } return score; }评估函数返回的是红方视角的分数搜索层用 negamax 时每一层都要取负。这里有个血泪经验子力价值别用太小的数否则位置分容易盖过子力差异AI 会为了占个好位置白丢一个车。我一般把车定在 900 以上兵定在 100位置奖励控制在子力价值的 10% 以内。2.4 搜索算法Alpha-Beta 剪枝与迭代加深搜索层是整个 AI 的大脑。最基础的是极小极大搜索但节点数随深度指数增长不加剪枝根本跑不动。Alpha-Beta 剪枝能把有效分支砍掉一半以上配合走法排序效果更明显。迭代加深则是从深度 1 开始搜逐层加深好处是随时可以按时间中断返回当前最优解。// Alpha-Beta 搜索depth 为剩余深度 int alphabeta(int depth, int alpha, int beta, int side) { if (depth 0) return evaluate() * side; Move moves[128]; int count 0; generate_all_moves(side, moves, count); // 走法排序吃子走法优先提高剪枝效率 sort_moves(moves, count); int best -INF; for (int i 0; i count; i) { make_move(moves[i]); int score -alphabeta(depth - 1, -beta, -alpha, -side); unmake_move(moves[i]); if (score best) best score; if (score alpha) alpha score; if (alpha beta) break; // 剪枝 } return best; }make_move和unmake_move必须严格配对前者修改棋盘并返回被吃棋子信息后者恢复。这里翻车最多的地方是忘记恢复被吃棋子导致搜索几层后棋盘上棋子凭空消失。走法排序用简单的“吃子价值高者优先”就能显著提升剪枝率不需要上复杂的置换表。搜索深度一般设 4 到 6 层再深在普通机器上等待时间会超过 3 秒体验反而下降。3. 从源码到可执行文件编译、运行与参数调整3.1 编译环境与命令项目没有第三方依赖Linux 下用 gcc 直接编译即可。Windows 下用 MinGW 或 Visual Studio 建空项目把源文件加进去也能编。常见做法是写一个简单的 Makefile把源文件列表和编译选项固定下来。# Linux / macOS 下编译 gcc -O2 -o chess main.c board.c movegen.c evaluate.c search.c # 如果源文件较多用通配符 gcc -O2 -o chess *.c -lm # Windows MinGW gcc -O2 -o chess.exe main.c board.c movegen.c evaluate.c search.c-O2优化对搜索性能影响很大实测能提升 30% 到 50% 的节点搜索速度。-lm是链接数学库如果代码里用了abs之外的数学函数需要加上。编译报错最常见的是函数隐式声明检查头文件是否都 include 了以及函数定义顺序是否在调用之前。3.2 运行流程与交互方式程序启动后一般先初始化棋盘然后进入主循环打印棋盘、等待玩家输入、校验走法、执行走法、调用 AI 搜索、执行 AI 走法、判断胜负。输入格式常见的是“起点坐标 终点坐标”比如7 1 7 4表示把炮从第 8 行第 2 列移到第 8 行第 5 列。坐标从 0 开始还是从 1 开始要看源码里的约定不一致会导致“明明能走却提示非法”。// 主循环简化示意 void game_loop() { init_board(); int side 1; // 红方先走 while (1) { print_board(); if (side 1) { // 人类玩家输入 int from_r, from_c, to_r, to_c; scanf(%d %d %d %d, from_r, from_c, to_r, to_c); Move m {pos(from_r, from_c), pos(to_r, to_c), 0}; if (!is_legal_move(m, side)) { printf(非法走法请重新输入\n); continue; } make_move(m); } else { // AI 搜索最佳走法 Move best search_best_move(side, 5); make_move(best); } if (is_checkmate(-side)) { printf(%s 方获胜\n, side 1 ? 红 : 黑); break; } side -side; } }is_legal_move需要做两层校验先检查走法是否符合兵种规则再检查走完后己方是否被将军。很多新手写的版本只做了第一层结果 AI 会走出“送帅”的走法。is_checkmate判断对方是否无子可走且被将军注意困毙无子可走但未被将军在象棋里也是输棋。3.3 调整 AI 强度的几个参数想让 AI 变强或变弱改三个地方就够了。搜索深度在search_best_move的调用参数里从 3 改到 6 会明显变强但耗时增加。评估函数里的子力价值表可以调把车的价值从 900 调到 1000AI 会更舍不得丢车。走法排序的权重也可以调吃子优先的权重越高剪枝越激进但可能漏掉一些安静的好棋。提示改搜索深度后一定要实测每步耗时深度 6 在低配机器上可能超过 5 秒玩家会以为程序卡死。4. 常见问题排查编译报错、走法异常、AI 卡死怎么查4.1 编译时报“未定义引用”现象链接阶段报undefined reference to evaluate之类的错误。原因通常是函数声明了但没实现或者实现所在的源文件没加入编译命令。解决检查 Makefile 或编译命令里的源文件列表确认每个声明的函数都有对应的.c文件并且该文件在编译列表里。如果函数在头文件里声明为static那它只能在定义它的文件里用跨文件调用要去掉static。4.2 走法生成出现“马穿墙”或“炮隔空吃子”现象马从棋盘一侧跳到另一侧或者炮中间没有炮架却吃掉了对方棋子。原因马的八个方向偏移量和马腿偏移量没有严格对应或者炮的扫描逻辑在遇到第一个棋子后没有正确切换到“找第二个棋子”的状态。解决把马的偏移量打印出来逐一核对确保每个方向的马腿位置在起点和目标点之间。炮的逻辑用两个标志位found_screen为假时遇到棋子置真为真时遇到棋子判断敌我。4.3 AI 思考时间过长或程序无响应现象玩家走完后程序卡住长时间不返回。原因搜索深度设得太高或者 Alpha-Beta 剪枝没生效导致节点爆炸。解决先用深度 3 测试确认能正常返回再逐步加深。检查剪枝条件alpha beta是否写对以及走法排序是否真的把吃子走法排在了前面。如果用了迭代加深加一个时间上限超时直接返回当前层最优解。4.4 棋盘显示错位或棋子符号混乱现象打印出来的棋盘行列对不齐或者红黑棋子符号反了。原因printf格式化时中文字符宽度和英文字符不一致或者棋子编码的正负号判断写反。解决用等宽字体或者把棋子符号换成单个英文字母如K代表帅k代表将。正负号判断统一用p 0表示红方p 0表示黑方不要混用。4.5 将军判断遗漏导致 AI 走出非法棋现象AI 走完后己方帅/将被对方吃掉但程序没有判负。原因is_legal_move只检查了兵种走法规则没有模拟走完后检查己方是否被将军。解决在is_legal_move里先make_move然后调用is_king_attacked检查己方帅/将是否处于被攻击状态最后unmake_move恢复。这个检查会增加一些开销但合法性必须保证。5. 二次开发改造换评估函数、加开局库、接图形界面5.1 替换评估函数提升棋力默认评估函数只考虑了子力和简单位置分改造成本最低、效果最明显的是加“棋子间协同”评分。比如车和马配合、炮和兵配合可以给额外加分。做法是在evaluate里遍历己方棋子对特定组合查表加分。// 协同加分示例车马配合 int synergy_bonus() { int bonus 0; for (int i 0; i BOARD_SIZE; i) { if (board[i] ! 5 board[i] ! -5) continue; // 只看车 for (int j 0; j BOARD_SIZE; j) { if (board[j] ! 4 board[j] ! -4) continue; // 只看马 if (board[i] * board[j] 0) { // 同方 int dist abs(i / 9 - j / 9) abs(i % 9 - j % 9); if (dist 3) bonus 15; // 车马靠近加分 } } } return bonus; }这个函数直接加进evaluate的返回值里注意红黑视角要对称。改完后用同一局面反复测试看 AI 选点是否更合理。我一般会存几个典型中局局面改一次评估就跑一遍对比 AI 的选点变化避免改出“负优化”。5.2 加开局库避免千篇一律搜索深度有限时AI 开局几步容易重复。加一个简单开局库用哈希表存“局面哈希 - 最佳走法”开局阶段先查库查不到再搜索。局面哈希用 Zobrist 哈希每个棋子在每个位置对应一个随机数异或起来就是当前局面哈希。// Zobrist 哈希表初始化 unsigned long long zobrist[BOARD_SIZE][15]; void init_zobrist() { srand(20240501); for (int i 0; i BOARD_SIZE; i) for (int j 0; j 15; j) zobrist[i][j] ((unsigned long long)rand() 32) | rand(); } // 计算当前局面哈希 unsigned long long compute_hash() { unsigned long long h 0; for (int i 0; i BOARD_SIZE; i) { if (board[i] 0) continue; int idx board[i] 0 ? board[i] : board[i] 8; h ^ zobrist[i][idx]; } return h; }开局库可以用文本文件存每行“哈希值 起点 终点”程序启动时加载进内存。注意哈希冲突概率虽然低但开局库条目少的时候可以直接用完整局面字符串做 key省去哈希计算。5.3 接图形界面或网络对战控制台版本跑通后想接图形界面最省事的是用 SDL2 或 EasyX 画棋盘和棋子。把print_board换成绘制函数把scanf输入换成鼠标点击坐标转换。核心搜索逻辑完全不用动只改输入输出层。如果想做网络对战把make_move前后的棋盘状态序列化通过 socket 发给对方对方反序列化后重建棋盘。注意网络传输要加校验防止非法走法。注意接图形界面后AI 搜索如果放在主线程会阻塞界面刷新常见做法是开一个单独线程跑搜索搜完后通过消息通知主线程更新棋盘。5.4 一个验证改造效果的小技巧每次改完评估函数或搜索参数别凭感觉判断好坏。我习惯用“自对弈”跑 20 局让新旧两个版本互相对战统计胜率。如果新版胜率明显高于 50%说明改动有效如果低于 50%大概率是改出了负优化。自对弈时关掉开局库随机性保证同一局面可复现。这个习惯帮我省了很多次“感觉变强了其实变弱了”的后悔药。从那以后我每次改评估函数都强制走一遍自对弈验证再决定要不要保留改动。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑