# chinese-chess **Repository Path**: ErGeAnan/chinese-chess-cpp ## Basic Information - **Project Name**: chinese-chess - **Description**: 一个基于 C++ 和 EasyX 图形库开发的中国象棋单机游戏,支持 **双人对战(1v1)** 与 **人机对战(Human vs AI)** 两种模式,AI 采用经典的 **极小极大搜索 + Alpha-Beta 剪枝** 算法。界面采用自定义图片资源,配备走子、吃子、将军、胜负等音效提示。 - **Primary Language**: C++ - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-08-11 - **Last Updated**: 2026-08-11 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 中国象棋(Chinese Chess C++) > 一个基于 C++17 和 EasyX 2023 图形库开发的中国象棋单机游戏,支持 **人机对战** 与 **双人对战** 两种模式,AI 采用经典的 **极小极大搜索 + Alpha-Beta 剪枝** 算法。 --- ## 目录 1. [项目概述](#1-项目概述) 2. [功能特性](#2-功能特性) 3. [项目结构](#3-项目结构) 4. [架构设计](#4-架构设计) 5. [核心模块详解](#5-核心模块详解) - 5.1 [数据结构与编码约定](#51-数据结构与编码约定) - 5.2 [棋盘与规则层:chess.h / chess.cpp](#52-棋盘与规则层chessh--chesscpp) - 5.3 [AI 搜索层:ai.h / ai.cpp](#53-ai-搜索层aih--aicpp) - 5.4 [界面与主循环:main.cpp](#54-界面与主循环maincpp) 6. [资源文件说明](#6-资源文件说明) 7. [编译构建指南](#7-编译构建指南) 8. [操作说明](#8-操作说明) 9. [核心算法深度解析](#9-核心算法深度解析) 10. [代码设计亮点](#10-代码设计亮点) 11. [扩展与二次开发](#11-扩展与二次开发) 12. [常见问题排查](#12-常见问题排查) 13. [附录 A:已知问题与缺失资源](#附录-a已知问题与缺失资源) 14. [附录 B:项目文件清单](#附录-b项目文件清单) --- ## 1. 项目概述 本项目是一个完整的、可编译运行的中国象棋对弈程序,具备以下核心能力: | 维度 | 说明 | |---|---| | **语言标准** | C++17 | | **图形库** | EasyX 2023(基于 Win32 GDI,仅支持 Windows) | | **编译器** | MSVC 2019/2022(需支持 C++17) | | **构建系统** | CMake 3.15+(推荐)/ 直接 MSVC cl.exe | | **AI 算法** | Minimax + Alpha-Beta 剪枝 + 走法排序 + 静态评估 | | **AI 强度** | 默认 5 层搜索(≈0.1-0.3s/步),可配置 4-6 层 | | **对战模式** | 人机对战(2 种) + 双人对战(1 种) | | **平台** | Windows 10/11(EasyX 仅支持 Windows) | | **许可证** | Apache License 2.0 | ### 技术特性一览 - **完整象棋规则**:七种棋子全部走法、蹩马腿、塞象眼、炮翻山、飞将、自将过滤、困毙/将死判定 - **双缓冲渲染**:`BeginBatchDraw / FlushBatchDraw` 避免画面闪烁 - **透明 PNG 渲染**:基于 Windows GDI `AlphaBlend` 实现棋子透明混合 - **异步音效**:点击/移动/吃子/将军/胜负音效,不阻塞主循环 - **棋盘翻转**:AI 先手模式自动翻转棋盘,确保玩家始终在屏幕下方 - **坐标标注**:棋盘四周标注行列号(1-9),方便定位 - **走法提示**:选中棋子后显示绿色落点/红色吃子圈,上一步高亮 --- ## 2. 功能特性 ### 2.1 三种对战模式 | 模式 | 描述 | 玩家执色 | AI 执色 | 棋盘显示 | |---|---|---|---|---| | **我先** (mode=0) | 人机对战,玩家红方先行 | 红(先手) | 黑 | 正常显示,红在下 | | **电脑先** (mode=1) | 人机对战,AI 红方先行 | 黑(后手) | 红 | 上下翻转,黑在下 | | **双人对战** (mode=2) | 同机双人轮流操作 | 红→黑交替 | 无 AI | 正常显示 | ### 2.2 完整规则实现 - **帅/将**:九宫内一步直行(上/下/左/右) - **仕/士**:九宫内一步斜行 - **相/象**:田字步(±2, ±2),不可过河,塞象眼判定 - **马**:日字步(8 种偏移),蹩马腿判定 - **车**:直线任意步,遇子即停,可吃敌方 - **炮**:移动同车,吃子必须中间恰好隔一个炮架 - **兵/卒**:未过河仅可前进一步,过河后新增左右各一格 - **飞将规则**:双方将帅同列且中间无子时,主动方判负 - **自将过滤**:走子后己方老将被对方攻击的走法视为非法 - **困毙判负**:无合法走法即判负(含被将死和无棋可走) ### 2.3 AI 智能决策 - **Minimax 搜索**:红方 MAX(最大化分数)、黑方 MIN(最小化分数)零和博弈 - **Alpha-Beta 剪枝**:维护 `alpha`(MAX 下界)与 `beta`(MIN 上界),当 `alpha >= beta` 时直接跳过剩余分支 - **走法排序**:按被吃子价值降序排列,让高价值吃子先搜索,提升剪枝效率 - **局面评估**:综合子力价值 + 位置加成(兵过河奖励、车马炮占中奖励) - **胜负终止**:`±100000 ± depth`,偏好最快致胜 / 最慢致败路径 ### 2.4 可视化交互 - 自定义棋盘与棋子 PNG 图片,棋子带 Alpha 透明通道 - 选中棋子时显示所有合法落点(绿色实心小圆点 / 红色空心圈吃子) - 上一步走法高亮(起点 + 终点叠加半透明选中框图片) - 将军时老将红色粗圈提示 - 底部状态栏实时显示当前回合 / AI 思考中 / 胜负信息 - 棋盘四周标注行列号,方便定位 ### 2.5 音效系统 - 鼠标点击音(CLICK.WAV) - 走子音(移动.WAV) - 吃子音(吃子.WAV / CAPTURE2.WAV) - 将军音(将.WAV / CHECK.WAV / CHECK2.WAV) - 胜负音(赢了.WAV / 输了.WAV) - 非法走子音(ILLEGAL.WAV) - 全部采用 `SND_ASYNC` 异步播放,不阻塞主循环 --- ## 3. 项目结构 ``` chinese-chess-cpp/ ├── CMakeLists.txt # CMake 构建配置(生成 MSVC 工程) ├── main.cpp # 主程序:图形渲染、菜单、输入处理、主循环 ├── chess.h # 棋盘类 ChessGame 声明 + 数据结构 ├── chess.cpp # 棋盘规则、走法生成、将军判定实现 ├── ai.h # AI 接口声明(findBestMove / evalMove) ├── ai.cpp # AI 算法:Minimax + Alpha-Beta + 评估函数 ├── run.bat # 一键构建运行脚本 ├── .gitignore # Git 忽略规则 ├── LICENSE # Apache License 2.0 │ ├── easyx/ # EasyX 2023 图形库 │ ├── include/ │ │ ├── .keep │ │ ├── easyx.h # 核心 API 声明 │ │ └── graphics.h # 图形 API(实际为空,内容合并在 easyx.h) │ └── lib/ │ └── .keep # lib 目录占位 │ └── img/ # 图片与音效资源(运行时需复制到 exe 同级) ├── .keep ├── bg.png # 棋盘背景(含交叉线、楚河汉界、外框) ├── bg.jpg # 菜单背景图 ├── bg拷贝.png # bg.png 备份副本(代码未引用) ├── fzcyjt.ttf # 字体文件(方正超细体) ├── r_box.png # 选中框/高亮框(半透明 PNG) ├── r_box2.png # 终点高亮框 ├── r_j.png ~ r_z.png # 红方 7 种棋子(帅仕相马车炮兵) ├── b_j.png ~ b_z.png # 黑方 7 种棋子(将士象马车炮卒) ├── CLICK.WAV # 鼠标点击音 ├── MOVE2.WAV # 走子音 ├── CAPTURE2.WAV # 吃子音 ├── CHECK.WAV / CHECK2.WAV # 将军音 ├── ILLEGAL.WAV # 非法走子音 └── ⚠️ 部分资源缺失(详见附录 A) ``` ### 文件职责总览 | 文件 | 代码行数 | 核心职责 | 对外接口 | |---|---|---|---| | `main.cpp` | ~740 | 窗口创建、资源加载、菜单、棋盘渲染、输入处理、主循环、音效 | `int main()` | | `chess.h` | ~108 | 数据结构定义、ChessGame 类声明 | 类接口文档 | | `chess.cpp` | ~404 | 七种棋子走法实现、将军判定、合法走法过滤、胜负判定 | `ChessGame` 方法实现 | | `ai.h` | ~41 | AI 接口声明 | `findBestMove()`, `evalMove()` | | `ai.cpp` | ~237 | 局面评估函数、Minimax 搜索、Alpha-Beta 剪枝 | AI 算法实现 | --- ## 4. 架构设计 ### 4.1 三层分离架构 本项目采用清晰的三层分离架构,各层之间通过接口通信,互不耦合: ``` ┌─────────────────────────────────────────────────────────┐ │ 界面层 (main.cpp) │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ 菜单绘制 │ │ 棋盘渲染 │ │ 输入处理 │ │ 音效播放 │ │ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ └─────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────┐ │ AI 搜索层 (ai.h / ai.cpp) │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ 局面评估 │ │ 走法排序 │ │ 搜索剪枝 │ │ │ └──────────┘ └──────────┘ └──────────┘ │ └─────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────┐ │ 规则/数据层 (chess.h / chess.cpp) │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ 棋盘表示 │ │ 走法生成 │ │ 将军判定 │ │ 胜负判定 │ │ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ └─────────────────────────────────────────────────────────┘ ``` ### 4.2 设计原则 1. **规则层独立**:`chess.h/cpp` 不依赖任何图形库和 AI 逻辑,可被命令行版、测试程序等复用 2. **AI 层独立**:`ai.h/cpp` 只依赖 `chess.h`,通过 `ChessGame` 接口操作棋盘,不关心渲染细节 3. **渲染与逻辑分离**:所有屏幕内容在每帧的 `drawGame()` 中从状态重新生成,`cleardevice` 不影响状态 4. **统一坐标变换**:所有格点→像素的换算通过 `cellX/cellY` 和 `dispR/realR` 函数封装 ### 4.3 数据流 ``` 玩家鼠标点击 → pixelToCell() → 棋盘格点 (r,c) │ ▼ genLegalMoves(turn) → 合法走法列表 │ ▼ 选中/落子 → makeMove() → 棋盘更新 │ ▼ checkGameOver() → 胜负判定 │ ▼ 下一帧 drawGame() 重绘所有内容 ``` AI 走子数据流: ``` 主循环检测 aiPending │ ▼ findBestMove(game, aiSide, depth) │ ├── genLegalMoves(aiSide) → 所有合法走法 ├── orderMoves() → 走法排序 ├── 对每个走法: │ ├── makeMove() │ ├── search(depth-1, ..., -aiSide) ← 递归搜索 │ │ ├── genLegalMoves(side) │ │ ├── orderMoves() │ │ ├── 对每个走法: makeMove → search(depth-2,...) → unmakeMove │ │ └── 剪枝判断 │ └── unmakeMove() └── 选出最优走法 │ ▼ makeMove(mv) → 棋盘更新 │ ▼ lastMove = mv; hasLastMove = true → 供绘制高亮 ``` --- ## 5. 核心模块详解 ### 5.1 数据结构与编码约定 #### 棋子编码 ```cpp enum PieceType { K=1, A=2, B=3, N=4, R=5, C=6, P=7 }; // K = 帅/将 (King) → 值 1 // A = 仕/士 (Advisor) → 值 2 // B = 相/象 (Bishop) → 值 3 // N = 马 (kNight) → 值 4 (避开 King 的 K) // R = 车 (Rook) → 值 5 // C = 炮 (Cannon) → 值 6 // P = 兵/卒 (Pawn) → 值 7 ``` 棋盘 `board[ROWS][COLS]` 编码规则: | 编码 | 含义 | |---|---| | **正数** (+1 ~ +7) | 红方棋子 | | **负数** (-1 ~ -7) | 黑方棋子 | | **0** | 空格 | 辅助方法: - `ChessGame::colorOf(p)` → 返回 1(红)/ -1(黑)/ 0(空) - `ChessGame::typeOf(p)` → 返回棋子类型 1..7(取绝对值) #### 棋盘坐标系 ``` 行坐标 r: 0 = 黑方底线(屏幕顶部), 9 = 红方底线(屏幕底部) 列坐标 c: 0 = 最左列, 8 = 最右列 列 0 1 2 3 4 5 6 7 8 行 0 ───────────────────── 黑方底线 行 1 │ │ 行 2 │ │ 黑炮 行 3 │ 卒 卒 卒 卒 卒 │ 行 4 │ │ ├──── 楚河 汉界 ──────┤ 行 5 │ │ 行 6 │ 兵 兵 兵 兵 兵 │ 行 7 │ │ 红炮 行 8 │ │ 行 9 ───────────────────── 红方底线 九宫: 红方(行7-9, 列3-5) / 黑方(行0-2, 列3-5) ``` #### 走法结构 ```cpp struct Move { int fromR, fromC; // 起点格点坐标 int toR, toC; // 终点格点坐标 int captured; // 被吃棋子(带符号,0 表示没吃子) }; ``` `captured` 字段的设计理由: 1. AI 搜索时 `makeMove/unmakeMove` 必须能完美撤销,需要保存被吃子信息 2. 界面层需要知道这步是否吃子,以决定播放哪种音效 ### 5.2 棋盘与规则层:chess.h / chess.cpp **文件位置**:[chess.h](file:///c:/Users/hp/Desktop/chinese-chess-cpp/chess.h)、[chess.cpp](file:///c:/Users/hp/Desktop/chinese-chess-cpp/chess.cpp) #### ChessGame 类接口 | 接口 | 类型 | 功能描述 | |---|---|---| | `reset()` | 公开方法 | 重置棋盘到初始标准布局。**必须先清零全部格子再摆放棋子**(修复了早期版本直接覆盖导致残留的 bug)。初始:红方先行 `turn=1`,共 32 子 | | `inBoard(r,c)` | 公开方法 | 判断坐标是否在 10×9 棋盘范围内 | | `colorOf(p)` | 静态方法 | 由棋子值取颜色:1=红,-1=黑,0=空 | | `typeOf(p)` | 静态方法 | 由棋子值取类型(取绝对值) | | `genPieceMoves(r,c,moves)` | 公开方法 | 生成单颗棋子的**伪合法走法**。伪合法=符合走子规则但不保证走后自将 | | `isAttacked(r,c,byColor)` | 公开方法 | 判断某格是否被指定颜色攻击。采用反向扫描而非枚举走法,效率更高 | | `findGeneral(side,gr,gc)` | 公开方法 | 找到指定颜色的将/帅位置。返回 false 表示将已被吃 | | `inCheck(side)` | 公开方法 | 判断是否被将军。含两种情况:①将所在格被对方攻击 ②飞将(双方将帅同列且中间无子) | | `genLegalMoves(side,moves)` | 公开方法 | 生成全部合法走法。对每个伪合法走法执行 makeMove→inCheck→unmakeMove,过滤掉自将走法 | | `makeMove(m)` | 公开方法 | 执行走子(不校验合法性,用于 AI 高频路径)。移动棋子+换边 | | `unmakeMove(m)` | 公开方法 | 撤销走子(与 makeMove 严格对应)。恢复棋子+恢复被吃子+换边 | | `checkGameOver()` | 公开方法 | 检查对局是否结束。两种判负:①任一方将不在→另一方胜 ②当前方无合法走法→当前方负 | #### 七种棋子走法实现 **帅/将**(K):[chess.cpp#L101-L111](file:///c:/Users/hp/Desktop/chinese-chess-cpp/chess.cpp#L101-L111) ```cpp // 九宫内一步直行(上下左右一格) // 红方九宫: 行 7..9, 列 3..5 // 黑方九宫: 行 0..2, 列 3..5 case K: { int rmin = (color == 1) ? 7 : 0; int rmax = (color == 1) ? 9 : 2; static const int kd[4][2] = { {1,0},{-1,0},{0,1},{0,-1} }; for (auto& d : kd) { int tr = r + d[0], tc = c + d[1]; if (tr < rmin || tr > rmax || tc < 3 || tc > 5) continue; add(tr, tc); } break; } ``` **仕/士**(A):[chess.cpp#L113-L123](file:///c:/Users/hp/Desktop/chinese-chess-cpp/chess.cpp#L113-L123) ```cpp // 九宫内一步斜行(4 个对角方向) case A: { int rmin = (color == 1) ? 7 : 0; int rmax = (color == 1) ? 9 : 2; static const int ad[4][2] = { {1,1},{1,-1},{-1,1},{-1,-1} }; for (auto& d : ad) { int tr = r + d[0], tc = c + d[1]; if (tr < rmin || tr > rmax || tc < 3 || tc > 5) continue; add(tr, tc); } break; } ``` **相/象**(B):[chess.cpp#L124-L139](file:///c:/Users/hp/Desktop/chinese-chess-cpp/chess.cpp#L124-L139) ```cpp // 田字步(±2,±2),不可过河,塞象眼判定 // 红相范围: 行 5..9(不过河到黑方) // 黑象范围: 行 0..4 case B: { int rmin = (color == 1) ? 5 : 0; int rmax = (color == 1) ? 9 : 4; static const int bd[4][2] = { {2,2},{2,-2},{-2,2},{-2,-2} }; for (auto& d : bd) { int tr = r + d[0], tc = c + d[1]; if (tr < rmin || tr > rmax || tc < 0 || tc > 8) continue; // 塞象眼: 田字中心点有子则不可走 int eyeR = r + d[0] / 2, eyeC = c + d[1] / 2; if (board[eyeR][eyeC] != 0) continue; add(tr, tc); } break; } ``` **马**(N):[chess.cpp#L140-L157](file:///c:/Users/hp/Desktop/chinese-chess-cpp/chess.cpp#L140-L157) ```cpp // 日字步(8 种偏移),蹩马腿判定 // 8 个方向: (±2,±1), (±1,±2) case N: { static const int nd[8][2] = { {2,1},{2,-1},{-2,1},{-2,-1},{1,2},{1,-2},{-1,2},{-1,-2} }; for (auto& d : nd) { int tr = r + d[0], tc = c + d[1]; if (!inBoard(tr, tc)) continue; // 蹩马腿: 沿"长边方向"紧邻一格非空则不可走 int legR, legC; if (d[0] == 2 || d[0] == -2) { legR = r + d[0] / 2; legC = c; // 行偏移2→马腿在同列 } else { legR = r; legC = c + d[1] / 2; // 列偏移2→马腿在同行 } if (board[legR][legC] != 0) continue; add(tr, tc); } break; } ``` **车**(R):[chess.cpp#L158-L172](file:///c:/Users/hp/Desktop/chinese-chess-cpp/chess.cpp#L158-L172) ```cpp // 直线任意步,遇任何子则停 // 若为敌方可吃,若为己方则停在该格之前 case R: { static const int rd[4][2] = { {1,0},{-1,0},{0,1},{0,-1} }; for (auto& d : rd) { int tr = r + d[0], tc = c + d[1]; while (inBoard(tr, tc)) { int tp = board[tr][tc]; if (tp == 0) moves.push_back({ r,c,tr,tc,0 }); // 空格直接走 else { if (colorOf(tp) != color) moves.push_back({ r,c,tr,tc,tp }); // 敌方可吃 break; // 己方或已吃则停 } tr += d[0]; tc += d[1]; } } break; } ``` **炮**(C):[chess.cpp#L173-L196](file:///c:/Users/hp/Desktop/chinese-chess-cpp/chess.cpp#L173-L196) ```cpp // 移动同车(不可越子);吃子必须恰好隔一个炮架 // 算法: 先找到第一个子作为炮架(screen=true), // 然后遇到第二个子时: 若为敌方则可吃,遇到后停止 case C: { static const int cd[4][2] = { {1,0},{-1,0},{0,1},{0,-1} }; for (auto& d : cd) { int tr = r + d[0], tc = c + d[1]; bool screen = false; while (inBoard(tr, tc)) { int tp = board[tr][tc]; if (!screen) { // 还没找到炮架 if (tp == 0) moves.push_back({ r,c,tr,tc,0 }); // 空格可走 else screen = true; // 第一个子=炮架 } else { // 已有炮架 if (tp != 0) { if (colorOf(tp) != color) moves.push_back({ r,c,tr,tc,tp }); // 吃子 break; // 遇到第二子后停止 } // 空格直接跳过 } tr += d[0]; tc += d[1]; } } break; } ``` **兵/卒**(P):[chess.cpp#L197-L211](file:///c:/Users/hp/Desktop/chinese-chess-cpp/chess.cpp#L197-L211) ```cpp // 未过河仅可前进一格;过河后可左右各一格 // 红方向"上"走(行减小),黑方向"下"走(行增大) case P: { int fwd = (color == 1) ? -1 : 1; if (inBoard(r + fwd, c)) add(r + fwd, c); bool crossed = (color == 1) ? (r <= 4) : (r >= 5); if (crossed) { if (inBoard(r, c - 1)) add(r, c - 1); if (inBoard(r, c + 1)) add(r, c + 1); } break; } ``` #### 将军判定实现 `isAttacked()` 采用反向扫描策略(从目标格向外扫描),效率高于枚举所有走法:[chess.cpp#L224-L279](file:///c:/Users/hp/Desktop/chinese-chess-cpp/chess.cpp#L224-L279) 1. **车/炮扫描**:四个正方向逐步扫描 - 遇到第一个非空子 → 若是对方车则被攻击 - 再遇到第二个非空子 → 若是对方炮则被攻击(炮架=第一个子) 2. **马的检测**:8 个日字位置反向检查,同时验证蹩马腿 3. **兵/卒检测**:检查紧邻的兵卒位置(未过河不能横攻) `inCheck()` 判定两种将军情况:[chess.cpp#L304-L321](file:///c:/Users/hp/Desktop/chinese-chess-cpp/chess.cpp#L304-L321) 1. 将所在格被对方攻击 2. **飞将**:双方将帅同列且中间无子 ### 5.3 AI 搜索层:ai.h / ai.cpp **文件位置**:[ai.h](file:///c:/Users/hp/Desktop/chinese-chess-cpp/ai.h)、[ai.cpp](file:///c:/Users/hp/Desktop/chinese-chess-cpp/ai.cpp) #### 外部接口 ```cpp // 为 aiSide 方寻找最佳走法(depth 层搜索) Move findBestMove(ChessGame& g, int aiSide, int depth); // 评估指定走法的分数(调试用) int evalMove(ChessGame& g, int aiSide, int depth, const Move& m); ``` #### 局面评估函数 **子力价值表**([ai.cpp#L31](file:///c:/Users/hp/Desktop/chinese-chess-cpp/ai.cpp#L31)): | 棋子类型 | K 帅将 | A 士仕 | B 象相 | N 马 | R 车 | C 炮 | P 兵卒 | |---|---|---|---|---|---|---|---| | 分值 | 10000 | 200 | 200 | 400 | 900 | 450 | 100 | - 将/帅权重极高(10000),宁可失所有其它子也不能丢老将 - 车 > 炮 > 马 > 士相 > 兵,参考通用象棋 AI 经验值 **兵卒位置加成**([ai.cpp#L47-L58](file:///c:/Users/hp/Desktop/chinese-chess-cpp/ai.cpp#L47-L58)): | 条件 | 加成 | |---|---| | 已过河 | +50 分 | | 越接近对方底线 | 每行 ±6 分 | | 靠近中路 (c=4) | 向两侧递减 2 分 | **车马炮位置加成**([ai.cpp#L66-L73](file:///c:/Users/hp/Desktop/chinese-chess-cpp/ai.cpp#L66-L73)):鼓励占据中线(列 4 最优),`mid + 4` 分。 **评估函数实现**([ai.cpp#L86-L100](file:///c:/Users/hp/Desktop/chinese-chess-cpp/ai.cpp#L86-L100)): ```cpp static int evaluate(const ChessGame& g) { int score = 0; for (int r = 0; r < ROWS; r++) for (int c = 0; c < COLS; c++) { int p = g.board[r][c]; if (p == 0) continue; int t = ChessGame::typeOf(p); int v = pieceValue[t]; if (t == P) v += pawnBonus(r, c, ChessGame::colorOf(p)); else v += positionBonus(r, c, t, ChessGame::colorOf(p)); if (ChessGame::colorOf(p) == 1) score += v; // 红方累加 else score -= v; // 黑方累减 } return score; } ``` #### 搜索算法核心 `search()` 函数是整个 AI 的核心,采用零窗口 Alpha-Beta 剪枝:[ai.cpp#L136-L179](file:///c:/Users/hp/Desktop/chinese-chess-cpp/ai.cpp#L136-L179) **终止条件**: 1. 当前方将不在棋盘 → 立即返回 ±100000±depth 2. `depth == 0` → 返回静态评估值 3. 无合法走法 → 返回 ±100000±depth(将死/困毙) **剪枝逻辑**: ```cpp // MAX 分支(红方 side==1) if (val > best) best = val; if (best > alpha) alpha = best; if (alpha >= beta) break; // 剪枝! // MIN 分支(黑方 side==-1) if (val < best) best = val; if (best < beta) beta = best; if (alpha >= beta) break; // 剪枝! ``` **胜负终止的深度微调**:`±100000 ± depth` 让 AI 选择最快致胜路径(赢越快越好),输棋时尽量拖延(希望对手犯错)。 #### 根节点实现 `findBestMove()` 对每个合法走法逐一执行 `makeMove → search(depth-1) → unmakeMove`,选出最优走法。根节点同样受益于 Alpha-Beta 剪枝。[ai.cpp#L195-L223](file:///c:/Users/hp/Desktop/chinese-chess-cpp/ai.cpp#L195-L223) ### 5.4 界面与主循环:main.cpp **文件位置**:[main.cpp](file:///c:/Users/hp/Desktop/chinese-chess-cpp/main.cpp) #### 布局常量 ```cpp const int WIN_W = 600, WIN_H = 740; // 窗口宽高 const int BOARD_X = 40, BOARD_Y = 35; // 棋盘图左上角 const int CELL = 56; // 相邻交点像素间距 const int MARGIN_X = 30, MARGIN_Y = 32; // 棋盘内第一个交点偏移 const int PIECE = 54; // 棋子图片边长 const int BAR_X = 0, BAR_Y = 610; // 底部状态栏位置 ``` **坐标变换函数**: ```cpp // 格点坐标 → 屏幕像素坐标 inline int cellX(int c) { return BOARD_X + MARGIN_X + c * CELL; } inline int cellY(int r) { return BOARD_Y + MARGIN_Y + r * CELL; } // 实际行 → 显示行(翻转模式下上下镜像) inline int dispR(int realR, bool flipBoard) { return flipBoard ? (9 - realR) : realR; } // 显示行 → 实际行(对称) inline int realR(int dispR_, bool flipBoard) { return flipBoard ? (9 - dispR_) : dispR_; } ``` **棋盘翻转逻辑**([main.cpp#L77-L82](file:///c:/Users/hp/Desktop/chinese-chess-cpp/main.cpp#L77-L82)): - 电脑先模式(mode=1):翻转棋盘,确保玩家(执黑)在屏幕下方 - 我先模式和双人模式:不翻转 #### 资源管理 所有 IMAGE 资源在程序启动时一次性加载:[main.cpp#L109-L135](file:///c:/Users/hp/Desktop/chinese-chess-cpp/main.cpp#L109-L135) | 全局变量 | 资源 | 尺寸 | |---|---|---| | `imgBoard` | bg.png | 原尺寸(不拉伸) | | `imgBox` | r_box.png | 原尺寸(选中框/起点高亮) | | `imgBox2` | r_box2.png | 原尺寸(终点高亮) | | `imgMenuBg` | bg.jpg | 600×740(窗口拉伸) | | `imgBar` | 底条.jpg | 600×90(状态栏) | | `imgBtnMe` | 我先.jpg | 420×130(菜单按钮) | | `imgBtnComp` | 电脑先.jpg | 420×130(菜单按钮) | | `imgPieces[2][8]` | 14 张棋子图 | 原尺寸 | #### 透明绘制 EasyX 的 `putimage` 不支持 Alpha 混合,因此自定义 `alphaImage()` 使用 Windows GDI `AlphaBlend`:[main.cpp#L158-L162](file:///c:/Users/hp/Desktop/chinese-chess-cpp/main.cpp#L158-L162) ```cpp static void alphaImage(int x, int y, IMAGE* src) { BLENDFUNCTION bf = { AC_SRC_OVER, 0, 255, AC_SRC_ALPHA }; AlphaBlend(GetImageHDC(NULL), x, y, src->getwidth(), src->getheight(), GetImageHDC(src), 0, 0, src->getwidth(), src->getheight(), bf); } ``` 需要链接 `msimg32.lib`(已在 CMake 配置)。 #### 菜单界面 `drawMenu()` 绘制主菜单,包含: - 标题"中 国 象 棋"(红色加粗字体) - 副标题"请选择对战模式" - 三个按钮(两个图片按钮 + 一个代码绘制的圆角矩形按钮) - 底部提示文字 `menuHit()` 通过矩形点击命中检测返回按钮 ID(0/1/2)。 #### 对局绘制 `drawGame()` 每帧绘制整个对局界面,层次如下(从下到上): | 层次 | 内容 | 说明 | |---|---|---| | 0 | 米黄色背景填充 | `solidrectangle` 消除黑边 | | 1 | 棋盘背景 `bg.png` | 原尺寸绘制,保证交叉点对齐 | | 2 | 坐标标注 | 列号(1-9)+ 行号(0-9),画在棋盘外围 | | 3 | 上一步走法高亮 | 起点 `r_box.png` + 终点 `r_box2.png` | | 4 | 选中棋子高亮 | 当前选中的己方棋子叠加 `r_box.png` | | 5 | 合法走法提示 | 空格=绿色实心小圆,吃子=红色空心圈 | | 6 | 全部棋子 | 遍历 `board[r][c]`,逐格 `alphaImage` | | 7 | 将军提示 | 受将方老将画红色粗圈 | | 8 | 底部状态栏 | 背景图 + 状态文字 | #### 主循环结构 ```cpp int main() { initgraph(WIN_W, WIN_H); loadAssets(); // 状态机: ST_MENU ↔ ST_PLAY State state = ST_MENU; ChessGame game; // ... 变量初始化 ... BeginBatchDraw(); while (running) { // 1. 绘制阶段 cleardevice(); if (state == ST_MENU) drawMenu(); else drawGame(game, selR, selC, selMoves, ...); FlushBatchDraw(); // 2. 输入阶段(非阻塞) while (peekmessage(&msg, EX_MOUSE | EX_KEY)) { // 左键: 菜单选择 / 选子 / 落子 // 右键: 取消选中 // R 键: 重置对局 // Esc 键: 返回菜单/退出 } // 3. AI 走子阶段(同步执行) if (aiPending && !game.gameOver) { Move mv = findBestMove(game, aiSide, AI_DEPTH); game.makeMove(mv); lastMove = mv; hasLastMove = true; playMoveSound(...); game.checkGameOver(); } Sleep(10); // 让出 CPU,约 100fps 上限 } EndBatchDraw(); closegraph(); } ``` #### 鼠标输入坐标反算 `pixelToCell()` 将鼠标像素坐标反算为棋盘格点:[main.cpp#L175-L187](file:///c:/Users/hp/Desktop/chinese-chess-cpp/main.cpp#L175-L187) ```cpp static bool pixelToCell(int mx, int my, int& r, int& c, bool flipBoard) { int x = mx - BOARD_X - MARGIN_X; // 棋盘内相对坐标 int y = my - BOARD_Y - MARGIN_Y; int dc = (int)std::round((double)x / CELL); // 最近的列 int dr = (int)std::round((double)y / CELL); // 最近的行 // 边界检查 if (dc < 0 || dc > 8 || dr < 0 || dr > 9) return false; // 距离检查(≤30 像素才算点中,避免误操作) int dx = x - dc * CELL, dy = y - dr * CELL; if (dx*dx + dy*dy > 30*30) return false; // 翻转还原 r = realR(dr, flipBoard); c = dc; return true; } ``` #### 音效系统 `playSnd()` 封装 `PlaySound` API,异步播放不阻塞: ```cpp static void playSnd(const TCHAR* path) { PlaySound(path, NULL, SND_FILENAME | SND_ASYNC); } ``` 走子音效优先级(`playMoveSound()`): 1. 对方被将军 → 播"将.WAV" 2. 本次吃了子 → 播"吃子.WAV" 3. 普通走子 → 播"移动.WAV" 胜负音在 `checkGameOver()` 之后单独播放。 #### 坐标标注绘制 `drawCoordinates()` 在棋盘外围绘制行列号: - **顶部列号**:屏幕从左到右 1-9(`colLabel(c) = c + 1`) - **底部列号**:屏幕从左到右 9-1(`9 - c`) - **左侧行号**:屏幕从上到下 0-9(`r`) - **右侧行号**:屏幕从上到下 9-0(`9 - r`) ### 5.5 隐藏的备用功能 `drawLastMoveHint()` 函数已实现但在 `drawGame()` 中被注释掉,因为作者觉得"太花不好看"。该函数使用蓝/橙色虚线圆框 + 箭头指示上一步走法,可随时启用:[main.cpp#L335-L360](file:///c:/Users/hp/Desktop/chinese-chess-cpp/main.cpp#L335-L360) ```cpp // 在 drawGame 中取消注释即可启用: // if (hasLastMove) drawLastMoveHint(lastMove, flipBoard); ``` --- ## 6. 资源文件说明 ### 6.1 棋子图片映射 | 类型 | 红方 | 含义 | 黑方 | 含义 | 尺寸 | |---|---|---|---|---|---| | K=1 | `r_j.png` | 帅 | `b_j.png` | 将 | 54×54 | | A=2 | `r_s.png` | 仕 | `b_s.png` | 士 | 54×54 | | B=3 | `r_x.png` | 相 | `b_x.png` | 象 | 54×54 | | N=4 | `r_m.png` | 马 | `b_m.png` | 马 | 54×54 | | R=5 | `r_c.png` | 车 | `b_c.png` | 车 | 54×54 | | C=6 | `r_p.png` | 炮 | `b_p.png` | 炮 | 54×54 | | P=7 | `r_z.png` | 兵 | `b_z.png` | 卒 | 54×54 | 命名规则:中文拼音首字母 j(将/帅)、s(士/仕)、x(象/相)、m(马)、c(车)、p(炮)、z(卒/兵)。前缀 `r_` 红、`b_` 黑。 ### 6.2 其它图片资源 | 文件 | 用途 | 加载方式 | 说明 | |---|---|---|---| | `bg.png` | 棋盘背景 | 原尺寸 | 含交叉线、楚河汉界、外框,需与 `MARGIN_X/Y` 对齐 | | `r_box.png` | 高亮框 | 原尺寸 | 半透明 PNG,用于选中框/起点高亮 | | `r_box2.png` | 终点框 | 原尺寸 | 终点高亮框 | | `bg.jpg` | 菜单背景 | 拉伸到 600×740 | 全屏背景 | | `底条.jpg` | 状态栏背景 | 拉伸到 600×90 | 底部横条 | | `我先.jpg` | 菜单按钮 | 拉伸到 420×130 | "我先"按钮图片 | | `电脑先.jpg` | 菜单按钮 | 拉伸到 420×130 | "电脑先"按钮图片 | ### 6.3 音效资源 | 文件 | 触发时机 | |---|---| | `CLICK.WAV` | 鼠标点击(菜单选择、棋子选中) | | `MOVE2.WAV` | 普通走子(不吃子、不将军) | | `CAPTURE2.WAV` | 吃子 | | `CHECK.WAV` / `CHECK2.WAV` | 将军 | | `ILLEGAL.WAV` | 非法走子操作 | | `赢了.WAV` | 胜利时(人机模式) | | `输了.WAV` | 失败时(人机模式) | ### 6.4 字体资源 `fzcyjt.ttf` - 方正超细体,用于菜单和状态栏文字渲染。 --- ## 7. 编译构建指南 ### 7.1 环境要求 | 项目 | 要求 | |---|---| | **操作系统** | Windows 10 / 11(EasyX 仅支持 Windows) | | **编译器** | Visual Studio 2019 / 2022(MSVC,需支持 C++17) | | **CMake** | 3.15 及以上(推荐方式) | | **EasyX** | 2023 版本(已内置在 `easyx/` 目录) | ### 7.2 方式一:CMake(推荐) ```bash # 1. 进入项目根目录 cd C:\Users\hp\Desktop\chinese-chess-cpp # 2. 配置 CMake(生成 VS 2022 工程) cmake -S . -B build -A x64 -DCMAKE_BUILD_TYPE=Release # 3. 编译 cmake --build build --config Release # 4. 运行 build\Release\chinese_chess.exe ``` **CMake 配置要点**: | 配置项 | 值 | 说明 | |---|---|---| | C++ 标准 | 17 | `CMAKE_CXX_STANDARD 17` | | 字符集 | Unicode | `UNICODE / _UNICODE` 宏 | | 禁用警告 | C4819 | `/wd4819` + `/utf-8` 避免编码问题 | | EasyX 库 | `EasyXw.lib` | Unicode 版本 | | 入口点 | `mainCRTStartup` | GUI 子系统用标准 main() | | 子系统 | WIN32 | 无控制台黑窗 | **链接的系统库**: `user32`, `gdi32`, `gdiplus`, `msimg32`, `ole32`, `shell32`, `advapi32`, `winmm` **构建后自动操作**:CMake 会将 `img/` 目录复制到 exe 同级目录,确保双击即可运行。 ### 7.3 方式二:run.bat 脚本 ```powershell cd C:\Users\hp\Desktop\chinese-chess-cpp .\run.bat ``` 该脚本会自动: 1. 检查 `build\Release\chinese_chess.exe` 是否已存在 2. 若不存在则自动执行 CMake 配置和编译 3. 编译完成后自动启动游戏 ### 7.4 方式三:Visual Studio 直接打开 1. 用 VS 打开 `CMakeLists.txt`(VS 2019/2022 内置 CMake 支持) 2. 选择 x64 Release 配置 3. 编译并运行 --- ## 8. 操作说明 ### 8.1 主菜单操作 | 操作 | 效果 | |---|---| | 左键点击"我先" | 进入对局,玩家执红先行 | | 左键点击"电脑先" | 进入对局,AI 执红先行,玩家执黑 | | 左键点击"双人对战" | 进入对局,同机双人轮流 | | Esc 键 | 退出程序 | ### 8.2 对局中操作 | 操作 | 功能 | |---|---| | **左键点击己方棋子** | 选中棋子,显示所有合法落点 | | **左键点击绿色圆点** | 将选中的棋子移动到该空格 | | **左键点击红色空心圈** | 吃掉敌方棋子 | | **左键点击己方其它棋子** | 切换选中目标 | | **右键点击棋盘** | 取消当前选中 | | **R 键** | 重置当前对局(保持原模式) | | **Esc 键** | 返回主菜单 | | **对局结束后任意点击** | 返回主菜单 | ### 8.3 界面提示说明 | 视觉元素 | 含义 | |---|---| | 半透明蓝色框(r_box.png) | 上一步走法的起点位置 | | 半透明橙色框(r_box2.png) | 上一步走法的终点位置 | | 半透明蓝色框(当前棋子) | 玩家选中的棋子 | | 绿色实心小圆点 | 可移动到的空格 | | 红色空心圆圈 | 可吃掉的敌方棋子 | | 红色粗圈 | 被将军的老将 | | 状态栏"红方/黑方:你的回合" | 轮到玩家走子 | | 状态栏"AI 思考中..." | AI 正在计算最佳走法 | | 状态栏"你赢了!" / "你输了!" | 对局结束 | ### 8.4 对局流程图 ``` ┌─────────┐ 选择模式 ┌─────────┐ │ 主菜单 │ ──────────────→ │ 对局中 │ └─────────┘ └────┬────┘ ▲ │ │ Esc / 对局结束点击 │ 走子/AI走子 │ ▼ │ ┌───────────┐ │ │ 检查胜负 │ │ └─────┬─────┘ │ │ │ ┌──────────┴──────────┐ │ ▼ ▼ │ ┌─────────┐ ┌─────────┐ │ │ 继续对局 │ │ 对局结束 │ │ └─────────┘ └────┬────┘ │ │ └─────────────────────────────────────────┘ 点击返回 ``` --- ## 9. 核心算法深度解析 ### 9.1 Minimax 极小极大原理 象棋是双人零和博弈:一方所得即为另一方所失。Minimax 原理假设双方都下出最优走法: ``` MAX (红方) 取最大分数 / | \ MIN(黑) MIN(黑) MIN(黑) ← 对手会选择对你最不利的走法 / \ / \ / \ 叶 叶 叶 叶 叶 叶 ← 评估函数打分 ``` 递归步骤: 1. 到达 `depth=0` 或胜负已分 → 调用 `evaluate()` 返回静态分数 2. 当前方是红方(MAX)→ 遍历所有走法,取最大分数 3. 当前方是黑方(MIN)→ 遍历所有走法,取最小分数 ### 9.2 Alpha-Beta 剪枝详解 **核心思想**:如果已经知道某分支不可能比当前已知的最优结果更好,就跳过它。 **两个边界值**: - `alpha`:MAX 层目前能保证的最好分数(下界) - `beta`:MIN 层目前能保证的最差分数(上界) **剪枝条件**:`alpha >= beta` **剪枝示例**(零窗口版本): ``` [alpha=-∞, beta=+∞] MAX (红方) / \ MIN(黑) → 剪枝! / \ 3 5 ↑ alpha=3 (更新) 3 >= beta=+∞? No, 继续 但下一个分支若能证明 MIN 方会选 ≤3 的结果, 则 MAX 方选择第一个走法即可,无需继续搜索。 ``` ### 9.3 走法排序的重要性 走法排序是 Alpha-Beta 剪枝效率的关键。本项目实现 `orderMoves()` 按被吃子价值降序排列: ```cpp static void orderMoves(std::vector& moves) { std::sort(moves.begin(), moves.end(), [](const Move& a, const Move& b) { return ChessGame::typeOf(a.captured) > ChessGame::typeOf(b.captured); }); } ``` 排序效果:让吃大车的走法先被搜索,能更快建立 `alpha` 边界,剪掉更多分支。在相同硬件上,好的走法排序可让 Alpha-Beta 多搜索 1-2 层。 ### 9.4 评估函数的设计哲学 象棋评估通常是"子力 + 位置 + 关系"的组合: | 维度 | 本项目实现 | 设计理由 | |---|---|---| | **子力价值** | 车900 炮450 马400 士相200 兵100 将10000 | 主干评估,保证 AI 不弃大子 | | **兵过河** | +50 分 | 过河兵威胁显著增大 | | **兵深入** | 每行 +6 分 | 越接近对方底线越危险 | | **中路控制** | 车马炮 +4~+12 分 | 中路控制是象棋开局核心 | | **将安全** | 将值 10000 | 宁可失所有子也不能失将 | **未实现的评估维度**(可扩展): - 兵种机动性(可走步数统计) - 马腿被蹩 / 象眼被塞的惩罚 - 车占据通路 / 骑河线的奖励 - 双马连环 / 炮架配合 - 将帅安全(士相完整性分析) ### 9.5 AI 走子可视化的踩坑经验 **问题**:早期版本在 AI `makeMove()` 之后直接 `alphaImage()` 画高亮框,下一帧 `cleardevice()` 把它清掉了。 **解决方案**:采用"状态持久化 + 统一帧内重绘"模式: ```cpp // 逻辑层:只改状态 lastMove = mv; hasLastMove = true; // 渲染层:每帧从状态重建画面 if (hasLastMove) { alphaImage(...); // 每帧都画,不会被 cleardevice 洗掉 } ``` 这是游戏引擎中经典的 **渲染与逻辑分离** 原则: - 逻辑层只修改状态变量 - 渲染层在每帧从状态生成全部屏幕内容 --- ## 10. 代码设计亮点 ### 10.1 规则层与界面层完全解耦 `chess.h/cpp` 不包含任何图形库头文件,可被命令行程序、测试框架等无差别复用。这使得将来开发 Web 版或手机版只需重写界面层。 ### 10.2 搜索的高频路径优化 AI 搜索每秒调用 `makeMove/unmakeMove` 数千次,采用了极简实现: ```cpp void ChessGame::makeMove(const Move& m) { board[m.toR][m.toC] = board[m.fromR][m.fromC]; board[m.fromR][m.fromC] = 0; turn = -turn; } ``` - 直接数组赋值,不做合法性校验 - `captured` 信息存在 `Move` 结构中,`unmakeMove` 直接恢复 ### 10.3 反向扫描的将军判定 `isAttacked()` 从目标格向外扫描,而非枚举所有走法: - 车/炮:方向扫描,遇到第一个子=车/第二个子=炮 - 马:8 个反向位置 + 蹩马腿检查 - 兵:近距离检查 这种 O(1)~O(8) 的扫描方式远优于枚举全局面走法的 O(N)。 ### 10.4 棋盘翻转的优雅实现 将棋盘翻转逻辑封装在 `dispR/realR` 函数中,棋盘数据层完全不受影响: ```cpp // 绘制时: 实际行 → 显示行 cellY(dispR(m.fromR, flipBoard)) // 输入时: 显示行 → 实际行 r = realR(dr, flipBoard); ``` 这个"镜像对称"设计让正/反两个变换函数完全相同,减少了出错可能。 ### 10.5 渲染与逻辑分离的状态持久化 所有需要在画面上叠加的临时标记(上一步高亮、合法走法提示等)都通过状态变量持久化: - `lastMove` / `hasLastMove`:上一步走法 - `selR` / `selC` / `selMoves`:当前选中状态 - `aiPending`:AI 是否正在思考 这确保了 `cleardevice` 后画面能完整重建。 ### 10.6 双缓冲无闪烁渲染 ```cpp BeginBatchDraw(); // ... 所有绘制操作 ... FlushBatchDraw(); // 一次性提交到屏幕 EndBatchDraw(); ``` 避免了逐行绘制造成的画面撕裂和闪烁。 --- ## 11. 扩展与二次开发 ### 11.1 AI 增强方向 | 技术 | 效果 | 实现难度 | |---|---|---| | **迭代加深** + **置换表(Zobrist Hash)** | 同等时间搜索更深 | 中 | | **静态搜索(Quiescence Search)** | 避免水平线效应 | 中 | | **历史启发** / **杀手启发** | 更精细的走法排序 | 低 | | **残局库** | 必胜/必和局面快速判定 | 高 | | **PVS 搜索** | 比 Alpha-Beta 更高效 | 中 | ### 11.2 评估函数升级 - 兵种机动性(统计可走步数) - 马腿被蹩 / 象眼被塞的惩罚 - 车占据通路 / 骑河线的奖励 - 将帅安全(士相完整性分析) - 双马连环 / 炮架配合的额外奖励 - 兵卒连线的评估 ### 11.3 功能特性扩展 | 功能 | 说明 | |---|---| | **悔棋** | 配合 `makeMove/unmakeMove` 天然支持,只需维护走法栈 | | **对局保存/加载** | 将 `Move` 数组序列化到文件 | | **AI 难度选择** | 将 `AI_DEPTH` 改为运行时配置(下拉选择 1-8 层) | | **走子动画** | 棋子从起点平滑移动到终点,插值每帧位置 | | **时间限制** | 每步限时,超时随机走一步 | | **走法回放** | 录入历史走法后自动回放对局 | ### 11.4 界面美化 - 棋子拖拽移动(目前是点选→点落) - 启用 `drawLastMoveHint()` 的箭头指示样式 - 高分辨率棋盘与棋子图片 - 走子粒子特效 - 主题切换(木纹/玻璃/水墨风格) ### 11.5 联网对战 将双人模式的鼠标输入来源从本地改为 Socket 接收即可扩展为网络对战: 1. 新增网络通信层(服务端/客户端) 2. 定义走法消息协议 3. 将双方 `ChessGame` 状态同步 ### 11.6 单元测试 规则层完全独立,便于编写单元测试: ```cpp // 测试兵过河后可横走 TEST(PawnTest, CrossedRiver) { ChessGame g; g.board[4][0] = P; // 红兵已过河 std::vector moves; g.genPieceMoves(4, 0, moves); // 应该包含向前和左右走法 EXPECT_EQ(moves.size(), 3); } ``` --- ## 12. 常见问题排查 | 错误现象 | 原因 | 解决方案 | |---|---|---| | **LNK2019: 无法解析的外部符号 WinMain** | WIN32 子系统默认入口是 WinMain,本项目用标准 main() | 确保 CMake 设置 `LINK_FLAGS "/ENTRY:\"mainCRTStartup\""` | | **LNK2019: 无法解析的外部符号 AlphaBlend** | 未链接 msimg32.lib | 确认 CMake 链接了 `msimg32` | | **C4819: 该文件包含不能在当前代码页(936)中表示的字符** | UTF-8 无 BOM 被 MSVC 按 GBK 解析 | CMake 已加 `/utf-8` 和 `/wd4819`;如仍报错可手动转为 UTF-8 with BOM | | **编译通过但运行黑屏** | img/ 目录未与 exe 同路径 | CMake 会自动复制;手动编译请将 img/ 复制到 exe 同级 | | **棋子黑底不透明** | 使用了 `putimage` 而非 `alphaImage` | 确认棋子绘制统一使用 `alphaImage()` | | **AI 走子提示不显示** | 早期版本直接画在屏幕被 cleardevice 清掉 | 当前代码已修复:用 `lastMove + hasLastMove` 状态持久化 | | **cmake --build 报 easyx.h 找不到** | EASYX_ROOT 路径不正确 | 确认 `easyx/include/easyx.h` 存在;可通过 `-DEASYX_ROOT=xxx` 指定 | | **点击棋盘无反应/点不中** | 鼠标坐标→格点换算有偏差 | 检查 `MARGIN_X/Y` 和 `CELL` 常量是否与棋盘图片匹配 | | **走子后棋子消失** | `board[r][c]` 数组索引越界 | 使用前验证坐标在 `[0,ROWS)×[0,COLS)` 内 | | **AI 思考时间过长** | `AI_DEPTH` 设得过大 | 修改 `main.cpp` 中的 `#define AI_DEPTH`,推荐 4-5 | | **程序启动报找不到 WAV** | 音效文件路径不对 | 确认 `img/` 目录与 exe 在同一目录 | --- ## 附录 A:已知问题与缺失资源 ### A.1 缺失的图片资源 代码中引用了以下图片文件,但当前 `img/` 目录中**不存在**这些文件: | 代码引用 | 用途 | 影响 | |---|---|---| | `img/底条.jpg` | 底部状态栏背景 | 状态栏显示异常(黑屏或缺图) | | `img/我先.jpg` | "我先"菜单按钮 | 按钮不显示或显示异常 | | `img/电脑先.jpg` | "电脑先"菜单按钮 | 按钮不显示或显示异常 | **修复方式**:自行准备对应尺寸的图片文件放入 `img/` 目录,或在代码中将 `loadimage` 调用改为加载现有图片(如 `bg.jpg` 拉伸)。 ### A.2 缺失的音效资源 代码中引用了以下 WAV 音效文件,但当前 `img/` 目录中**不存在**这些文件: | 代码引用 | 用途 | 已有替代文件 | |---|---|---| | `img/移动.WAV` | 普通走子音 | `img/MOVE2.WAV` | | `img/吃子.WAV` | 吃子音 | `img/CAPTURE2.WAV` | | `img/将.WAV` | 将军音 | `img/CHECK.WAV` / `img/CHECK2.WAV` | | `img/赢了.WAV` | 胜利音 | 无 | | `img/输了.WAV` | 失败音 | 无 | **修复方式**: 1. 在代码中修改 `playSnd()` 调用,使用现有文件: - `_T("img/移动.WAV")` → `_T("img/MOVE2.WAV")` - `_T("img/吃子.WAV")` → `_T("img/CAPTURE2.WAV")` - `_T("img/将.WAV")` → `_T("img/CHECK.WAV")` 2. 或自行录制/获取中文命名的 WAV 文件放入 `img/` 目录 3. 胜负音(赢了.WAV / 输了.WAV)需自行添加 ### A.3 未使用的文件 | 文件 | 说明 | |---|---| | `img/bg拷贝.png` | 疑似 `bg.png` 的备份副本,代码未引用 | ### A.4 EasyX 库文件缺失 `easyx/lib/` 目录当前为空(仅有 `.keep` 占位文件)。EasyX 需要以下库文件: - `EasyXa.lib`(ANSI 版本) - `EasyXw.lib`(Unicode 版本,本项目使用) **修复方式**:从 [EasyX 官网](https://easyx.cn) 下载完整版本,将 `EasyXw.lib` 放入 `easyx/lib/` 目录。 --- ## 附录 B:项目文件清单 | 文件 | 行数 | 主要内容 | |---|---|---| | `main.cpp` | 740 | 窗口、菜单、棋盘渲染、输入、主循环、音效 | | `chess.h` | 108 | 数据结构(PieceType, Move)、ChessGame 类声明 | | `chess.cpp` | 404 | 7 种棋子走法、将军判定、合法走法、make/unmake | | `ai.h` | 41 | findBestMove / evalMove 接口声明 | | `ai.cpp` | 237 | 评估函数、Minimax、Alpha-Beta 剪枝 | | `CMakeLists.txt` | 67 | 构建配置 | | `run.bat` | 14 | 一键构建运行脚本 | | `easyx/include/easyx.h` | 368 | EasyX 图形库 API 声明 | --- > **项目特色**:代码按"规则/AI/界面"三层清晰解耦,改动一处不会牵连另外两层。结构清晰、逻辑自洽,适合作为学习 C++ 游戏编程和 AI 算法的参考项目。