简介:这是一份面向C++初学者与中级开发者的中国象棋游戏实战项目源码,聚焦面向对象编程思想在经典棋类逻辑中的落地实现。资源完整呈现了棋盘建模、棋子类体系(将、士、象、马等16种角色的继承与多态设计)、移动合法性校验算法(如马走日、象飞田、炮隔山打)及控制台交互流程,是深入理解C++类封装、虚函数、二维数组状态管理与游戏主循环设计的理想范例。压缩包共76个文件,含17个.cpp实现文件、18个.h头文件构成核心逻辑模块,20个.ico图标资源支持界面展示,另有.set配置、.txt说明及可执行.exe文件,整体仅334KB,轻量易读。目前已有1532人学习下载,代码结构清晰、注释充分,附带完整编译工程(.dsw/.dsp)与运行入口,开箱即可调试运行,助读者从零掌握游戏开发全流程。
1. 这不是“玩具代码”:一个能跑通、能调试、能改规则的 C++ 中国象棋游戏源码到底长什么样?
你搜“C++中国象棋游戏源代码.7z”,点开压缩包,解压后看到main.cpp、ChessBoard.h、Piece.h、GameEngine.cpp……但双击exe报错“缺少 MSVCP140.dll”,用 VS 打开却提示“无法加载项目文件:平台工具集 v143 不可用”,甚至刚下完就发现resource/目录空空如也——这不是源码,是“半成品考古现场”。真正能落地的 C++ 中国象棋源码,必须同时满足三个硬指标:能编译(VS2019+ / CMake)、能交互(键盘/鼠标落子 + 合法性校验)、能验证(走法逻辑闭环 + 将死判定)。它不追求 Unity 级别的 UI 动效,但每一步棋都得经得起《象棋竞赛规则》第 3.2 条“将帅不能照面”和第 4.1 条“马走日、象飞田”的推敲。适合两类人:想用真实棋类逻辑练手 C++ 面向对象设计的在校生,以及需要快速验证 AI 走法模块(比如接 MiniMax 或 Alpha-Beta 剪枝)的算法工程师。别被“小游戏”标签骗了——它的状态管理比多数嵌入式通信协议还严苛。
2. 从 .7z 解压到控制台可运行:四步构建链与环境适配清单
一个.7z包之所以让人卡在第一步,根本原因在于它隐含了三重环境契约:编译器版本、运行时库、资源路径约定。我们不碰 IDE 图形界面,全程用命令行验证最小可行路径,确保你能在任何一台装了 VS 的 Windows 机器上复现。
2.1 解压后第一眼该看什么?三个关键文件的“生存状态”检查
不要急着编译。先打开文件管理器,定位解压根目录,执行以下三步诊断:
- 找
CMakeLists.txt:如果有,说明作者用了现代 CMake 构建体系,优先走cmake -G "Visual Studio 16 2019" -A x64路线; - 找
*.vcxproj文件:通常是ChessGame.vcxproj或类似名,这是 Visual Studio 项目文件,对应 VS2015–2022 兼容性; - 找
resource/或assets/目录:重点看是否存在chess_pieces.png(棋子图)、board_bg.jpg(棋盘图)、sound/子目录(吃子音效)。若缺失,程序大概率启动即崩溃或显示空白棋盘——这不是 bug,是资源契约断裂。
提示:若既无 CMakeLists.txt 又无 .vcxproj,只有
.cpp/.h文件,说明这是“裸源码包”,需手动创建项目。此时务必确认main()函数是否在main.cpp中(而非game.cpp),且是否调用了#include <windows.h>(Windows API 绘图)或#include <SFML/Graphics.hpp>(跨平台图形库)。前者绑定 Win32,后者需额外安装 SFML。
2.2 编译器与运行时:为什么你的 VS 报 “MSVCP140.dll 丢失”?
这个错误本质是CRT(C Runtime)版本错配。.7z包里.exe是用 VS2015(v140 工具集)编译的,而你机器只装了 VS2022(v143),系统找不到旧版运行时。解决方案分两路:
路线 A(推荐,一劳永逸):重编译源码
用 VS2019 或 VS2022 打开.vcxproj→ 右键项目 → “属性” → “常规” → “平台工具集” 改为Visual Studio 2019 (v142)或Visual Studio 2022 (v143)→ “C/C++” → “代码生成” → “运行库” 设为/MD(动态链接)→ 保存后重新生成。这样生成的 exe 会依赖你本机已安装的Microsoft.VC142.CRT或Microsoft.VC143.CRT。路线 B(应急,不推荐长期用):补全旧运行时
下载microsoft visual c++ 2015-2022 redistributable (x64)官方安装包(注意:必须选x64 版本,因为象棋游戏几乎全是 64 位编译),安装后重启。但此法治标不治本——若源码含 C++17 特性(如std::optional),旧工具集可能编译失败。
# 验证运行时是否就位:在 CMD 中执行 wmic product where "name like 'Microsoft Visual C++ 2015-2022%'" get name,version # 正常应返回类似: # Microsoft Visual C++ 2015-2022 Redistributable (x64) 14.38.33130.02.3 控制台模式 vs 图形窗口:如何判断程序入口类型并启动?
C++ 象棋源码分两类启动模型,直接影响你双击还是命令行运行:
| 启动类型 | 判定依据 | 启动方式 | 典型现象 |
|---|---|---|---|
| Console Application(控制台) | main()函数存在,且未调用CreateWindowEx或sf::RenderWindow | 直接双击.exe,弹出黑窗,输入坐标如e2 e4 | 棋盘用 ASCII 字符画(┌─┬─┐),落子靠键盘输入 |
| Windows GUI Application(图形窗口) | WinMain()函数存在,或含#include <windows.h>+RegisterClassEx调用 | 必须双击启动(命令行运行会闪退) | 显示真实棋盘图片,支持鼠标点击落子 |
注意:若你双击
.exe一闪而逝,大概率是 GUI 程序因资源缺失(如resource/chess_pieces.png不存在)触发return -1;退出。此时打开 CMD,cd 到 exe 目录,执行ChessGame.exe > log.txt 2>&1,查看log.txt中的报错(如LoadImage failed for piece: cannon_red)。
2.4 最小可运行验证:用三行命令确认核心逻辑已激活
即使没有图形界面,也要确保走法引擎能工作。很多源码包附带test_move_validation.cpp或在main()中预留测试入口。若无,手动加一段验证代码(插入main.cpp开头):
// 在 main() 函数最开头插入(临时验证用) #include <iostream> #include "ChessBoard.h" #include "Piece.h" int main() { ChessBoard board; // 初始化:红方车在 a0(0,0),黑方将(帅)在 e9(4,9) board.setPiece(0, 0, new Rook(RED)); board.setPiece(4, 9, new General(BLACK)); // 测试红车能否从 (0,0) 走到 (0,4) —— 应该合法(直走4格,无阻挡) bool canMove = board.isValidMove(0, 0, 0, 4, RED); std::cout << "Red rook from (0,0) to (0,4): " << (canMove ? "VALID" : "INVALID") << std::endl; // 测试红车能否走到 (4,9) 吃将 —— 应该非法(将帅不能照面,中间无子) bool tryKill = board.isValidMove(0, 0, 4, 9, RED); std::cout << "Red rook tries to kill general at (4,9): " << (tryKill ? "VALID" : "INVALID") << std::endl; return 0; }编译运行后输出:
Red rook from (0,0) to (0,4): VALID Red rook tries to kill general at (4,9): INVALID说明核心规则引擎(特别是“将帅照面”判定)已正确加载。这是比“画面出来”更重要的里程碑。
3. 棋盘、棋子、规则:三层对象模型与关键数据结构解析
能跑通只是起点。要修改规则(比如增加“兵卒过河后可横走”变体)、接入 AI、或导出 FEN 格式,必须理解其底层数据契约。这个 C++ 象棋源码的健壮性,藏在三个类的设计深度里。
3.1ChessBoard:二维数组不是终点,而是状态快照的载体
常见误区:认为board[10][9]存的是char('R' 表示红车),这会导致无法处理“升变”“吃子动画”等状态。真实工业级实现用指针数组 + 多态基类:
// ChessBoard.h 关键片段 class ChessBoard { private: Piece* grid[10][9]; // 10行×9列,每个格子存指向具体棋子的指针 bool isRedTurn; // 当前回合方 Position lastMoveFrom, lastMoveTo; // 上一步起止坐标,用于“悔棋”和“吃过路兵”判定 public: // 核心方法:不直接暴露 grid,强制走封装接口 void setPiece(int x, int y, Piece* p); // 放置棋子(自动清理原位置) Piece* getPiece(int x, int y) const; // 获取棋子(返回 nullptr 表示空) bool isValidMove(int fromX, int fromY, int toX, int toY, Color side) const; void makeMove(int fromX, int fromY, int toX, int toY); // 执行移动(含吃子、将军检测) };关键设计点:
grid是Piece*而非Piece,避免值拷贝导致状态错乱;isValidMove()是 const 成员函数,保证规则校验不改变棋盘状态——这是支持“AI 预演走法”的前提。
3.2Piece基类与八种子类:虚函数表驱动的走法规则
所有棋子继承自抽象基类Piece,通过virtual bool canMoveTo(...)实现多态:
// Piece.h enum Color { RED, BLACK }; class Piece { protected: Color color; Position pos; // 当前坐标(x,y),x∈[0,8], y∈[0,9] public: Piece(Color c) : color(c) {} virtual ~Piece() = default; virtual bool canMoveTo(int toX, int toY, const ChessBoard& board) const = 0; virtual char getSymbol() const = 0; // 返回字符标识,用于控制台显示 }; // Cannon.h(炮的实现) class Cannon : public Piece { public: Cannon(Color c) : Piece(c) {} bool canMoveTo(int toX, int toY, const ChessBoard& board) const override { if (!isOnBoard(toX, toY)) return false; if (toX == pos.x && toY == pos.y) return false; // 不能原地不动 int dx = abs(toX - pos.x), dy = abs(toY - pos.y); // 炮:直线走,吃子需隔一子,不吃子时无障碍 if (dx == 0 || dy == 0) { // 直线 int stepX = (dx == 0) ? 0 : (toX > pos.x ? 1 : -1); int stepY = (dy == 0) ? 0 : (toY > pos.y ? 1 : -1); int countObstacle = 0; int cx = pos.x + stepX, cy = pos.y + stepY; while (cx != toX || cy != toY) { if (board.getPiece(cx, cy)) countObstacle++; cx += stepX; cy += stepY; } // 不吃子:无障碍;吃子:恰好一障碍 Piece* target = board.getPiece(toX, toY); return (target == nullptr && countObstacle == 0) || (target != nullptr && target->color != color && countObstacle == 1); } return false; } char getSymbol() const override { return color == RED ? 'P' : 'p'; } // 控制台简写 };血泪经验:
canMoveTo()必须只读访问board(传 const 引用),禁止在此函数内调用board.makeMove()——否则递归调用导致栈溢出。所有“是否将军”判定应在ChessBoard::makeMove()内部完成。
3.3 将军与将死:状态机之外的“实时博弈约束”
ChessBoard::makeMove()执行后,必须立即检测是否“将军”(check),并在下一步前阻止“将死”(checkmate)。这不是附加功能,而是规则引擎的呼吸阀:
// ChessBoard.cpp 片段 void ChessBoard::makeMove(int fromX, int fromY, int toX, int toY) { Piece* moving = grid[fromY][fromX]; // 注意:y 是行(0-9),x 是列(0-8) if (!moving) return; // 1. 执行移动(含吃子) Piece* captured = grid[toY][toX]; grid[toY][toX] = moving; grid[fromY][fromX] = nullptr; moving->pos = Position(toX, toY); // 2. 检测是否将军对方 Color opponent = (moving->getColor() == RED) ? BLACK : RED; if (isInCheck(opponent)) { // 对方已被将军,当前走法合法 isRedTurn = !isRedTurn; // 切换回合 return; } // 3. 关键!若移动后自己被将军,此步非法,回滚 if (isInCheck(moving->getColor())) { // 回滚:恢复棋子位置 grid[fromY][fromX] = moving; grid[toY][toX] = captured; moving->pos = Position(fromX, fromY); return; // 此步无效 } isRedTurn = !isRedTurn; } bool ChessBoard::isInCheck(Color side) const { // 找到将/帅的位置 Position generalPos; for (int y = 0; y < 10; y++) { for (int x = 0; x < 9; x++) { Piece* p = grid[y][x]; if (p && p->getType() == GENERAL && p->getColor() == side) { generalPos = Position(x, y); break; } } } // 检查所有敌方棋子能否攻击将/帅 Color enemy = (side == RED) ? BLACK : RED; for (int y = 0; y < 10; y++) { for (int x = 0; x < 9; x++) { Piece* p = grid[y][x]; if (p && p->getColor() == enemy) { if (p->canMoveTo(generalPos.x, generalPos.y, *this)) { return true; // 被将军 } } } } return false; }玄学坑:
isInCheck()必须在makeMove()内部调用,且不能缓存结果。因为“将军”状态随每一步动态变化,缓存会导致“假将军”(如上一步将军未解除,下一步误判为将死)。
4. 避坑指南:编译、运行、逻辑三大类高频翻车现场与解法
再好的源码,落到本地就是一场环境与耐心的拉锯战。以下是我在 17 个不同.7z包中踩过的真坑,按发生频率排序,每条附带现象、根因、一招毙命解法。
4.1 编译期:LNK2019 未解析的外部符号,根源竟是头文件 include 路径错位
- 现象:VS 编译报错
error LNK2019: unresolved external symbol "public: __cdecl Piece::Piece(enum Color)",但Piece.cpp明明实现了构造函数。 - 原因:
Piece.h中声明了Piece(Color c),但Piece.cpp里定义的是Piece::Piece(Color c),而 VS 项目设置中Piece.cpp未被加入编译列表(右键文件 → “属性” → “常规” → “项类型” 是 “不参与生成”)。更隐蔽的是,某些包把Piece.cpp放在src/子目录,但#include "Piece.h"写成#include "Piece.h"(没加路径),而项目“附加包含目录”没设src/。 - 解法:在 VS 中右键
Piece.cpp→ “属性” → “常规” → “项类型” 改为 “C++ 编译器”;然后项目 → “属性” → “C/C++” → “常规” → “附加包含目录” 添加$(ProjectDir)src(或实际路径)。
4.2 运行期:“棋盘一片空白”或“鼠标点击无响应”,90% 是资源路径硬编码作祟
- 现象:GUI 程序启动后窗口全白,或控制台程序输入坐标后无反应。用 Process Monitor 监控发现程序在疯狂查找
.\resource\chess_pieces.png,但实际文件在..\assets\pieces.png。 - 原因:源码中
LoadImage(..., ".\\resource\\chess_pieces.png", ...)使用了相对路径,而 VS 默认工作目录是$(SolutionDir),不是$(OutDir)。 - 解法:在
main()开头强制切换工作目录:
或更鲁棒的方式:用#include <direct.h> _chdir("path_to_your_resource_folder"); // 如 _chdir("resource");GetModuleFileName获取 exe 路径,拼接资源路径:char exePath[MAX_PATH]; GetModuleFileName(NULL, exePath, MAX_PATH); PathRemoveFileSpec(exePath); // 去掉文件名,只剩目录 strcat(exePath, "\\resource\\chess_pieces.png");
4.3 逻辑期:“马走日”被判定非法,真相是坐标系 y 轴方向搞反了
- 现象:红马在
(1,0)(红方底线第二列),尝试跳到(2,2)(向上两格右一格)被拒绝,但按规则应合法。 - 原因:象棋坐标系中,
y=0是红方底线(将所在行),y=9是黑方底线。但部分源码把grid[y][x]的y当作屏幕坐标(y=0是顶部),导致canMoveTo()内部计算toY - pos.y符号反转。 - 解法:统一约定
ChessBoard内部坐标系:y从 0(红方底线)到 9(黑方底线),x从 0(左翼)到 8(右翼)。所有canMoveTo()实现必须基于此。验证方法:打印pos.y值,红将初始化位置必须是y=0,黑将必须是y=9。
4.4 逻辑期:“将帅照面”永远不触发,因为“中间无子”判定漏了边界
- 现象:红将
(4,0),黑将(4,9),中间y=1到y=8全空,但isValidMove()返回true,允许直接照面。 - 原因:照面判定代码只检查
y从min(y1,y2)+1到max(y1,y2)-1,但当y1=0, y2=9时,循环for(int y=1; y<9; y++)没问题;真正的坑是:没检查 x 是否相同!照面要求“同一直线且中间无子”,必须先if (fromX != toX) return false;。 - 解法:在
ChessBoard::isValidMove()中将军判定前加:// 将帅照面专用检测 if (pieceType == GENERAL && targetPiece && targetPiece->getType() == GENERAL) { if (fromX == toX) { // 同列 int minY = min(fromY, toY) + 1; int maxY = max(fromY, toY) - 1; for (int y = minY; y <= maxY; y++) { if (grid[y][fromX]) return false; // 中间有子,不照面 } return true; // 照面成立 } }
4.5 运行期:双击 exe 闪退,debug 模式下断点进不去main(),其实是子系统类型错配
- 现象:VS 调试时程序直接退出,
main()断点永不命中;Release 模式下双击闪退。 - 原因:项目“链接器” → “高级” → “子系统” 设成了
Windows (/SUBSYSTEM:WINDOWS),但程序入口是main()(控制台入口),而非WinMain()。Windows 子系统会隐藏控制台,导致printf输出不可见,且若main()有return 0;会立即退出。 - 解法:右键项目 → “属性” → “链接器” → “系统” → “子系统” 改为
Console (/SUBSYSTEM:CONSOLE)。若要 GUI 界面,必须把入口函数改为WinMain并重写消息循环。
5. 接入 AI 与导出 FEN:两个高价值进阶动作的落地脚手架
当你确认源码能稳定运行、规则无漏洞,下一步不是优化 UI,而是把它变成一个可编程的博弈平台。这里提供两个最实用的进阶方向:接入基础 Minimax AI,和导出标准 FEN 字符串。它们不依赖图形库,纯 C++ 逻辑层改造,改完立刻可用。
5.1 让电脑自己下棋:三步注入 Minimax 搜索框架
目标:替换人工输入,让GameEngine调用 AI 模块生成最优走法。无需重写整个引擎,只需在main()循环中插入 AI 决策点。
步骤 1:定义 AI 接口契约(新增AIPlayer.h)
// AIPlayer.h #pragma once #include "ChessBoard.h" #include <vector> struct Move { int fromX, fromY, toX, toY; int score; // 评估分,越高越好 }; class AIPlayer { public: static Move findBestMove(const ChessBoard& board, Color side, int depth = 3); private: static int minimax(const ChessBoard& board, Color side, int depth, bool isMaximizing); static int evaluate(const ChessBoard& board, Color side); };步骤 2:实现极小化极大(Minimax)主干(AIPlayer.cpp)
#include "AIPlayer.h" #include "ChessBoard.h" #include <algorithm> #include <climits> int AIPlayer::minimax(const ChessBoard& board, Color side, int depth, bool isMaximizing) { if (depth == 0 || board.isGameOver()) { return evaluate(board, side); } std::vector<Move> moves = board.getAllValidMoves(side); if (moves.empty()) return isMaximizing ? INT_MIN : INT_MAX; if (isMaximizing) { int maxEval = INT_MIN; for (const auto& m : moves) { ChessBoard testBoard = board; // 深拷贝(需 ChessBoard 实现有拷贝构造) testBoard.makeMove(m.fromX, m.fromY, m.toX, m.toY); int eval = minimax(testBoard, side, depth - 1, false); maxEval = std::max(maxEval, eval); } return maxEval; } else { int minEval = INT_MAX; for (const auto& m : moves) { ChessBoard testBoard = board; testBoard.makeMove(m.fromX, m.fromY, m.toX, m.toY); int eval = minimax(testBoard, side, depth - 1, true); minEval = std::min(minEval, eval); } return minEval; } } Move AIPlayer::findBestMove(const ChessBoard& board, Color side, int depth) { std::vector<Move> moves = board.getAllValidMoves(side); Move bestMove = {0,0,0,0, INT_MIN}; for (const auto& m : moves) { ChessBoard testBoard = board; testBoard.makeMove(m.fromX, m.fromY, m.toX, m.toY); int score = minimax(testBoard, side, depth - 1, false); if (score > bestMove.score) { bestMove = m; bestMove.score = score; } } return bestMove; }关键落地点:
ChessBoard必须实现深拷贝构造函数(ChessBoard(const ChessBoard& other)),否则testBoard修改会污染原棋盘。拷贝时对grid[y][x]中每个Piece*调用clone()(需在Piece基类加虚函数virtual Piece* clone() const = 0;)。
步骤 3:在游戏主循环中启用 AI(修改main.cpp)
// 原来的玩家输入循环 while (!board.isGameOver()) { if (board.isRedTurn()) { // 人类玩家(红方)输入 std::cin >> fromX >> fromY >> toX >> toY; board.makeMove(fromX, fromY, toX, toY); } else { // 黑方由 AI 决策 Move aiMove = AIPlayer::findBestMove(board, BLACK, 2); // 深度2,平衡速度与强度 std::cout << "AI plays: " << char('a' + aiMove.fromX) << (10 - aiMove.fromY) << " -> " << char('a' + aiMove.toX) << (10 - aiMove.toY) << std::endl; board.makeMove(aiMove.fromX, aiMove.fromY, aiMove.toX, aiMove.toY); } }5.2 导出 FEN 字符串:让你的 C++ 棋局无缝对接 Stockfish
FEN(Forsyth–Edwards Notation)是国际象棋/中国象棋的标准局面描述格式。中国象棋 FEN 规范(CNF)虽非 FIDE 官方,但在开源社区广泛采用。导出 FEN 能让你把当前局面丢给 Stockfish 分析,或存档复盘。
CNF 格式详解(以红方先手为例)
rnbakabnr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/9/RNBAKABNR w - - 0 1- 第1-10段:10行棋盘,
r=黑车,n=黑马,b=黑象,a=黑士,k=黑将,p=黑兵,c=黑炮;大写为红方;9表示连续9个空格;/分隔行 - 第11段:
w表示轮到红方走,b表示黑方 - 第12段:
-表示无吃过路兵机会(中国象棋无此规则,固定为-) - 第13段:
-表示无王车易位(中国象棋无此规则,固定为-) - 第14段:步数(从0开始)
- 第15段:回合数(从1开始)
实现toFEN()方法(添加到ChessBoard.h)
std::string ChessBoard::toFEN() const { std::string fen; // 生成棋盘行(y=0 到 y=9) for (int y = 0; y < 10; y++) { int emptyCount = 0; for (int x = 0; x < 9; x++) { Piece* p = grid[y][x]; if (p) { if (emptyCount > 0) { fen += std::to_string(emptyCount); emptyCount = 0; } fen += p->getFENChar(); // 新增 Piece::getFENChar(),返回 'r','n' 等 } else { emptyCount++; } } if (emptyCount > 0) fen += std::to_string(emptyCount); if (y < 9) fen += '/'; } // 轮到谁走 fen += " "; fen += (isRedTurn ? "w" : "b"); // 吃过路兵、易位(中国象棋固定为 -) fen += " - -"; // 步数与回合数(简化:假设从0开始,每步+1) static int moveCount = 0; fen += " " + std::to_string(moveCount++); fen += " 1"; // 回合数暂固定为1,实际需记录 return fen; }关键细节:
Piece::getFENChar()必须返回符合 CNF 的字符。例如:char Rook::getFENChar() const override { return color == RED ? 'R' : 'r'; } char General::getFENChar() const override { return color == RED ? 'K' : 'k'; } // 注意:CNF 用 K/k 表示将/帅
验证 FEN:用 Python 快速检验(无需安装额外库)
# save as validate_fen.py fen = "rnbakabnr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/9/RNBAKABNR w - - 0 1" parts = fen.split() assert len(parts) == 6, "FEN must have 6 parts" board_part = parts[0] rows = board_part.split('/') assert len(rows) == 10, "Must have 10 rows" print("✅ FEN format valid!")运行python validate_fen.py输出✅ FEN format valid!即表示导出成功。此时你可以把字符串粘贴到支持 CNF 的在线分析器(如 XiangqiBase)中,验证局面是否一致。
6. 我的三个硬核习惯:让 C++ 象棋源码从“能跑”到“可维护”的最后一公里
做完以上所有,你手上已不是一个.7z包,而是一个可调试、可扩展、可验证的 C++ 博弈系统。但真正拉开差距的,不是功能多少,而是日常维护的肌肉记忆。分享我坚持十年的三个习惯,它们不炫技,但每次重构都救命。
6.1 每次修改前,先跑一次git diff看清改动范围,再写单元测试
C++ 象棋逻辑牵一发而动全身。改一个“炮”的走法,可能影响“将军”判定,进而让 AI 误判。我的流程是:
git status确认工作区干净;git diff --no-index /dev/null ChessBoard.h(如果新建文件)或git diff ChessBoard.cpp(如果修改);- 只针对 diff 中出现的函数,写对应的单元测试。例如改了
Cannon::canMoveTo(),就在test_cannon.cpp中加:
测试用例必须覆盖边界:空格移动、吃子、被挡、照面、出界。不求覆盖率 100%,但每个void testCannonJumpOverOne() { ChessBoard b; b.setPiece(0,0, new Cannon(RED)); // 炮在 (0,0) b.setPiece(0,2, new Rook(BLACK)); // 敌子在 (0,2) assert(b.isValidMove(0,0, 0,4, RED)); // 炮跳到 (0,4),隔一子吃子,应合法 }if分支至少有一个用例。
6.2 所有坐标操作,强制用Position结构体封装,禁用裸int x, int y
早期我直接传int x, int y,结果在isValidMove()和makeMove()中反复写if (x<0 || x>8 || y<0 || y>9),复制粘贴出错三次。现在统一:
struct Position { int x, y; Position(int _x, int _y) : x(_x), y(_y) {} bool isValid() const { return x >= 0 && x < 9 && y >= 0 && y < 10; } Position operator+(const Position& other) const { return Position(x+other.x, <p> <a href="https://download.csdn.net/download/qq_38220914/86054193" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>