本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:本项目是一个基于C/C++语言开发的中国象棋游戏实例,提供完整可下载的源代码,旨在帮助开发者深入理解使用C/C++实现复杂游戏逻辑的全过程。项目涵盖棋盘绘制、棋子规则、用户交互、游戏状态管理等核心功能,涉及数据结构、算法设计、面向对象编程及可能的图形界面开发。通过学习和修改该源码,开发者可掌握游戏开发中的关键编程技术,提升在实际项目中应用C/C++的能力。
c/c++项目-中国象棋实例源码下载

1. C/C++基础语法与程序结构

C/C++作为系统级编程语言,其高效性与灵活性使其成为开发高性能象棋引擎的理想选择。本章将回顾C/C++中最核心的语法要素,包括变量类型、控制流、函数定义与指针操作,重点强调内存管理与数组访问的安全性。理解这些基础知识是后续构建复杂数据结构和实现象棋逻辑的前提。例如,通过指针直接操作棋盘数组可提升性能,但需警惕越界访问:

int board[10][9]; // 定义标准象棋棋盘
if (row >= 0 && row < 10 && col >= 0 && col < 9) {
    board[row][col] = piece; // 安全赋值
}

2. 棋盘与棋子的数据结构设计

在开发中国象棋这类策略性棋类游戏时,合理的数据结构设计是整个系统稳定运行和高效运算的基础。良好的数据抽象不仅能提升代码的可读性和维护性,还能为后续实现复杂的规则判断、AI决策以及图形界面交互提供坚实支撑。本章将围绕“棋盘”与“棋子”两大核心实体展开深入探讨,从底层存储方式到高层逻辑封装,逐步构建一个兼具性能与扩展性的数据模型。

2.1 棋盘的逻辑建模与存储方式

棋盘作为所有棋子活动的舞台,其建模质量直接影响走法检测、状态更新乃至人工智能评估函数的效率。中国象棋的标准棋盘由9列10行构成(共90个交叉点),采用二维坐标系进行定位。在程序中如何高效地表示这一结构,并支持快速访问、边界检查及坐标转换,是本节的核心议题。

2.1.1 使用二维数组表示棋盘布局

最直观且高效的棋盘建模方法是使用 二维数组 来映射物理棋盘上的每一个位置。对于C/C++语言而言,静态或动态分配的 int board[10][9] 即可完整覆盖所有合法落子点。每个元素存储指向某个棋子对象的指针或标识符,从而实现“位置—棋子”的绑定关系。

// 示例:使用整型编码表示棋子状态
enum PieceType {
    EMPTY = 0,
    RED_KING, RED_GUARD, RED_ELEPHANT, RED_HORSE, RED_CHARIOT, RED_CANNON, RED_SOLDIER,
    BLACK_KING, BLACK_GUARD, BLACK_ELEPHANT, BLACK_HORSE, BLACK_CHARIOT, BLACK_CANNON, BLACK_SOLDIER
};

PieceType board[10][9]; // 10行 × 9列 的棋盘数组

上述代码定义了一个类型枚举 PieceType ,并通过 board[10][9] 数组记录每一点的状态。该方案的优点在于内存连续、缓存友好,访问时间为常量 $O(1)$,非常适合频繁查询的场景,如移动合法性验证。

内存布局与访问效率分析

二维数组在内存中按 行优先顺序 连续存储,这意味着相邻行的同一列元素之间存在较大的地址跳跃。例如 board[i][j] board[i+1][j] 在内存中相隔9个元素的距离。这种特性对缓存命中率有一定影响,尤其在遍历列方向时可能导致较多缓存未命中。

为了优化访问模式,可以考虑以下策略:
- 转置存储 :以列为主序重新组织数据;
- 平铺一维数组 :使用 PieceType* board = new PieceType[90]; 并通过 index = i * 9 + j 计算索引;
- 分块预取 :在热点循环中手动提示编译器预加载数据。

尽管如此,在大多数实际应用中,二维数组因其简洁性和可读性仍是最优选择。

存储方式 空间复杂度 访问速度 可读性 扩展性
二维数组 O(n²) ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐☆ ⭐⭐☆☆☆
一维数组+索引计算 O(n²) ⭐⭐⭐⭐☆ ⭐⭐☆☆☆ ⭐⭐⭐☆☆
哈希表映射 O(k), k<90 ⭐⭐☆☆☆ ⭐⭐⭐☆☆ ⭐⭐⭐⭐⭐

注:k为当前有子的位置数;适用于稀疏棋局(如残局模拟)

graph TD
    A[开始游戏] --> B[初始化棋盘数组]
    B --> C{是否红方先手?}
    C -->|是| D[设置红方初始布局]
    C -->|否| E[设置黑方初始布局]
    D --> F[填充board[][]值]
    E --> F
    F --> G[进入主循环]

该流程图展示了基于二维数组的棋盘初始化流程,体现了从逻辑设定到具体内存写入的过程一致性。

初始化代码示例与参数说明
void initializeBoard(PieceType board[10][9]) {
    // 清空棋盘
    for (int i = 0; i < 10; ++i)
        for (int j = 0; j < 9; ++j)
            board[i][j] = EMPTY;

    // 设置红方初始布局 (底部)
    board[0][4] = RED_KING;
    board[0][3] = board[0][5] = RED_GUARD;
    board[0][2] = board[0][6] = RED_ELEPHANT;
    board[0][1] = board[0][7] = RED_HORSE;
    board[0][0] = board[0][8] = RED_CHARIOT;
    board[2][1] = board[2][7] = RED_CANNON;
    for (int j = 0; j < 9; j += 2)
        board[3][j] = RED_SOLDIER;

    // 设置黑方初始布局 (顶部)
    board[9][4] = BLACK_KING;
    board[9][3] = board[9][5] = BLACK_GUARD;
    board[9][2] = board[9][6] = BLACK_ELEPHANT;
    board[9][1] = board[9][7] = BLACK_HORSE;
    board[9][0] = board[9][8] = BLACK_CHARIOT;
    board[7][1] = board[7][7] = BLACK_CANNON;
    for (int j = 0; j < 9; j += 2)
        board[6][j] = BLACK_SOLDIER;
}

逐行解读与逻辑分析
- 第3–6行:双重循环将整个棋盘清零,确保无残留数据。
- 第9–14行:红方位于第0行,按照“将→士→象→马→车→炮→兵”的顺序布阵;其中兵仅出现在第3行奇数列。
- 第16–21行:黑方镜像布置,起始于第9行,其余规则一致。
- 所有赋值均使用枚举常量,增强语义清晰度并避免魔法数字。

此初始化过程不仅建立了正确的初始局面,也为后续回合控制、胜负判定提供了可靠起点。

2.1.2 棋盘坐标系的设计与转换规则

在中国象棋中,用户通常习惯使用“代数记谱法”,如“炮二平五”、“马8进7”等。这些符号依赖于一种特定的坐标体系——即横向用阿拉伯数字编号(1~9),纵向用中文数字或步数描述。然而在程序内部,必须将其转化为统一的 数组索引坐标 才能进行运算。

我们设计如下映射规则:

象棋记法 行号 i 列号 j
底部第0行(红方) i=0 j=8 → 左侧为1,右侧为9
顶部第9行(黑方) i=9 j=0 → 左侧为1,右侧为9

注意:红黑双方视角相反,因此需引入 阵营相关坐标翻转机制 。例如红方认为“右三路”对应 j=6,而黑方相同物理位置也应视为“右三路”。

为此,定义通用转换函数:

struct Position {
    int row, col; // 内部数组坐标 [0..9][0..8]
};

// 将用户输入的“第r行第c列”(从1开始)转换为内部坐标
Position toInternalCoord(int inputRow, int inputCol, bool isRedSide) {
    Position pos;
    pos.row = isRedSide ? (10 - inputRow) : (inputRow - 1); // 红方倒序,黑方正序
    pos.col = inputCol - 1; // 列统一减1
    return pos;
}

// 反向转换:用于输出提示信息
std::pair<int, int> toUserCoord(int internalRow, int internalCol, bool isRedSide) {
    int userRow = isRedSide ? (10 - internalRow) : (internalRow + 1);
    int userCol = internalCol + 1;
    return {userRow, userCol};
}

参数说明
- inputRow , inputCol :用户输入的棋盘行列(从1开始计数)
- isRedSide :指示当前操作方颜色,决定行坐标的翻转逻辑
- 返回值 Position :标准化的内部索引,直接用于数组访问

该设计解决了双重视角下的坐标歧义问题,使得无论哪一方操作,都能正确解析“前进一步”或“左移一路”的意图。

此外,在GUI开发中还需与像素坐标进行映射:

// 假设每个格子宽高为60px,左上角偏移(50,50)
Point pixelToBoard(float px, float py, float offsetX=50, float offsetY=50, float cellSize=60) {
    int col = (int)((px - offsetX) / cellSize);
    int row = (int)((py - offsetY) / cellSize);
    if (row >= 0 && row < 10 && col >= 0 && col < 9)
        return {row, col};
    else
        throw std::invalid_argument("点击位置超出棋盘范围");
}

该函数实现了鼠标点击到棋盘点的精准捕捉,是人机交互的关键桥梁。

2.1.3 数组边界处理与访问优化策略

由于棋盘尺寸固定但某些移动规则涉及越界探测(如车直线扫描、马走日字蹩腿检测),必须建立安全的边界防护机制,防止数组越界引发崩溃。

边界检查宏定义与封装
#define VALID_POS(r, c) ((r) >= 0 && (r) < 10 && (c) >= 0 && (c) < 9)

bool canMoveTo(const PieceType board[10][9], int fromR, int fromC, int toR, int toC) {
    if (!VALID_POS(toR, toC)) return false;
    PieceType target = board[toR][toC];
    PieceType source = board[fromR][fromC];
    // 同色不能吃
    if (target != EMPTY && isSameColor(source, target))
        return false;
    return true;
}

逻辑分析
- VALID_POS 宏避免重复编写条件判断,提高代码复用性;
- canMoveTo 函数结合目标位置是否存在同色棋子做进一步过滤;
- 此类封装有助于集中管理非法访问风险。

循环展开与SIMD优化建议

对于高频调用的扫描操作(如炮的吃子路径检测),可采用 循环展开 减少分支预测失败:

// 示例:车沿x轴正方向扫描
for (int dx = 1; dx < 9 - j; ++dx) {
    int x = i, y = j + dx;
    if (!VALID_POS(x, y)) break;
    if (board[x][y] != EMPTY) {
        addLegalMove(i, j, x, y); // 可吃
        break;
    }
    addLegalMove(i, j, x, y); // 可达空位
}

若进一步追求极致性能,可借助 SIMD指令集 对整行/列进行批量比对,但需谨慎权衡可移植性与开发成本。

优化技术 提升幅度 适用场景 实现难度
边界宏封装 +10% 所有访问操作
编译器自动向量化 +15~30% 连续扫描类算法
手动SIMD优化 +50%+ AI搜索中的快速评估
分块缓存预取 +20% 多线程并行搜索

最终,通过合理选择优化手段,在保证安全的前提下最大限度释放硬件潜力。

3. 面向对象编程在象棋游戏中的应用

面向对象编程(Object-Oriented Programming, OOP)是现代软件工程中不可或缺的范式之一,尤其在构建结构清晰、可扩展性强的游戏系统时,其封装性、继承性和多态性三大核心特性发挥着关键作用。中国象棋作为一种规则明确、状态复杂、行为多样化的博弈系统,天然适合作为OOP实践的理想场景。通过将“棋子”、“棋盘”、“走法规则”等实体抽象为类,并合理划分职责,不仅提升了代码的模块化程度,也为后续功能扩展(如AI对战、网络联机)打下坚实基础。

本章将深入探讨如何运用C++语言特性,在象棋游戏中实现一套完整的面向对象架构。从基础类的设计开始,逐步引入继承与多态机制以支持不同兵种的行为差异,最终通过信息隐藏原则保障系统的安全性与稳定性。整个设计过程遵循高内聚、低耦合的原则,确保每一部分都能独立演化而不影响整体结构。

3.1 类的基本设计与职责划分

在面向对象设计中,合理的类划分是系统健壮性的前提。对于象棋游戏而言,最核心的两个实体是 棋子 棋盘 。它们分别承担个体行为描述与全局状态管理的职责。通过对这两个实体进行类抽象,可以建立起清晰的对象模型,便于后续逻辑实现与维护。

3.1.1 ChessPiece类:封装棋子行为与状态

ChessPiece 类作为所有具体棋子的基类或直接实现类,负责统一管理每个棋子的基本属性与通用行为。该类的设计应体现数据与操作的紧密结合,符合封装的基本原则。

3.1.1.1 成员变量设计与构造函数实现

一个典型的 ChessPiece 类需要包含以下成员变量:

成员变量 类型 说明
color PlayerColor 棋子所属方(红/黑),使用枚举类型定义
type PieceType 棋子种类(将、士、象、马、车、炮、兵)
x , y int 当前在棋盘上的坐标位置
alive bool 存活状态,被吃后置为 false
id int 唯一标识符,用于调试或序列化
enum class PlayerColor { RED, BLACK };
enum class PieceType { GENERAL, ADVISOR, ELEPHANT, HORSE, CHARIOT, CANNON, PAWN };

class ChessPiece {
private:
    PlayerColor color;
    PieceType type;
    int x, y;
    bool alive;
    int id;

public:
    // 构造函数
    ChessPiece(PlayerColor c, PieceType t, int startX, int startY, int pieceId)
        : color(c), type(t), x(startX), y(startY), alive(true), id(pieceId) {}

    // 获取方法(Getter)
    PlayerColor getColor() const { return color; }
    PieceType getType() const { return type; }
    int getX() const { return x; }
    int getY() const { return y; }
    bool isAlive() const { return alive; }
    int getId() const { return id; }

    // 设置位置
    void setPosition(int newX, int newY) {
        x = newX;
        y = newY;
    }

    // 标记死亡
    void kill() { alive = false; }
};

代码逻辑逐行解读分析:

  • 第1–2行:定义两个强类型枚举 PlayerColor PieceType ,增强类型安全与可读性。
  • 第6–14行:声明私有成员变量,外部无法直接访问,体现信息隐藏。
  • 第17–21行:构造函数初始化所有字段,保证对象创建时处于合法状态。
  • 第24–35行:提供公共 getter 方法供外部查询状态,避免暴露内部数据。
  • 第38–42行: setPosition 允许更新棋子坐标,模拟移动行为。
  • 第45行: kill() 方法标记棋子已被吃掉,不影响内存释放,仅逻辑删除。

这种设计使得 ChessPiece 成为一个轻量级、自包含的状态单元,任何上层逻辑(如走法判断、渲染)都可以通过标准接口与其交互。

3.1.1.2 虚函数机制支持多态移动逻辑

尽管基本属性相同,但不同兵种的移动方式截然不同。例如,“马走日”、“炮打隔子”。若在 ChessPiece 中硬编码每种走法,则会导致大量条件分支,违反开闭原则。为此,引入虚函数机制,使派生类能够重写各自的移动规则。

修改后的类声明如下:

class ChessPiece {
    // ... 其他成员不变 ...

public:
    virtual ~ChessPiece() = default;

    // 纯虚函数:判断是否可以从 (fromX, fromY) 移动到 (toX, toY)
    virtual bool isValidMove(int toX, int toY, const ChessBoard& board) const = 0;

    // 可选:获取所有合法目标点(用于AI生成走法)
    virtual std::vector<std::pair<int, int>> getValidMoves(const ChessBoard& board) const = 0;
};

参数说明:
- toX , toY :目标位置坐标;
- board :传入当前棋盘引用,用于检测路径障碍、敌方棋子等上下文信息;
- 返回值:布尔值表示该移动是否合法。

通过将 isValidMove 定义为纯虚函数,强制所有继承者必须实现自己的走法逻辑。这为后续多态调用奠定基础。

设计优势: 使用接口隔离策略,将“什么是棋子”与“如何移动”解耦。未来新增兵种(如自定义变体棋)只需新增派生类,无需修改已有代码。

3.1.2 ChessBoard类:管理全局棋局状态

如果说 ChessPiece 是细胞级别的单位,那么 ChessBoard 就是整个身体的中枢神经系统。它负责维护棋盘布局、协调棋子间的相互作用,并对外提供统一的操作入口。

3.1.2.1 棋子容器组织与更新接口

ChessBoard 的核心任务之一是高效地组织所有棋子。常见做法有两种:
1. 使用二维数组记录每个位置上的棋子指针;
2. 使用容器(如 std::vector<std::unique_ptr<ChessPiece>> )集中管理所有活动棋子。

推荐采用 组合方式 :既保留二维索引快速定位能力,又用智能指针统一生命周期管理。

#include <array>
#include <memory>
#include <vector>

class ChessBoard {
private:
    static constexpr int BOARD_ROWS = 10;
    static constexpr int BOARD_COLS = 9;

    // 二维数组:存储指向棋子的指针(空位为 nullptr)
    std::array<std::array<std::unique_ptr<ChessPiece>, BOARD_COLS>, BOARD_ROWS> grid;

    // 所有活跃棋子的列表(便于遍历)
    std::vector<std::unique_ptr<ChessPiece>> allPieces;

public:
    ChessBoard();
    // 初始化初始布局
    void setupInitialPosition();

    // 获取指定位置的棋子
    ChessPiece* getPieceAt(int x, int y) const;

    // 移动棋子(注意:需先验证合法性)
    bool movePiece(int fromX, int fromY, int toX, int toY);

    // 删除某个位置的棋子(吃子)
    void removePieceAt(int x, int y);

    // 添加新棋子(如复活、摆谱)
    void addPiece(std::unique_ptr<ChessPiece> piece, int x, int y);
};

代码逻辑逐行解读分析:
- 第7–8行:定义常量尺寸,符合象棋标准棋盘(10×9);
- 第11行:二维 std::array 结构,每个元素是 unique_ptr<ChessPiece> ,自动管理内存;
- 第14行:额外维护一个动态容器,方便批量处理(如查找所有红方棋子);
- 第19行:构造函数负责初始化空棋盘;
- 第22行: setupInitialPosition() 加载初始布阵;
- 第25行: getPieceAt() 提供安全访问,越界检查应在内部完成;
- 第28行: movePiece() 是高层操作,通常调用底层验证后再执行;
- 第31–34行:增删接口用于实现吃子、悔棋等功能。

3.1.2.2 提供查询与修改棋盘的方法

为了支持规则引擎与用户交互, ChessBoard 必须暴露一系列查询接口。例如:

// 查询某一方的所有棋子
std::vector<ChessPiece*> ChessBoard:: getPiecesByColor(PlayerColor color) const {
    std::vector<ChessPiece*> result;
    for (const auto& piece : allPieces) {
        if (piece->isAlive() && piece->getColor() == color) {
            result.push_back(piece.get());
        }
    }
    return result;
}

// 判断某位置是否有棋子
bool ChessBoard:: isEmpty(int x, int y) const {
    return x >= 0 && x < BOARD_ROWS && y >= 0 && y < BOARD_COLS && grid[x][y] == nullptr;
}

此外,还可加入事件通知机制(观察者模式),当棋盘状态变化时自动触发UI刷新或日志记录。

性能提示: 若频繁查询某类棋子(如“所有炮”),可引入哈希表缓存索引,牺牲空间换取时间。

classDiagram
    class ChessPiece {
        <<abstract>>
        -PlayerColor color
        -PieceType type
        -int x, y
        -bool alive
        -int id
        +getColor() PlayerColor
        +getType() PieceType
        +getX(), getY() int
        +isAlive() bool
        +setPosition(x, y)
        +kill()
        +isValidMove(toX, toY, board) bool*
        +getValidMoves(board) vector~pair~
    }

    class KingPiece {
        +isValidMove(...) bool
        +getValidMoves(...) vector~pair~
    }

    class HorsePiece {
        +isValidMove(...) bool
        +getValidMoves(...) vector~pair~
    }

    class ChessBoard {
        -array~array~ grid
        -vector~unique_ptr~ allPieces
        +setupInitialPosition()
        +getPieceAt(x,y) ChessPiece*
        +movePiece(fromX,fromY,toX,toY) bool
        +removePieceAt(x,y)
        +addPiece(piece,x,y)
        +getPiecesByColor(color) vector~ChessPiece*~
        +isEmpty(x,y) bool
    }

    ChessBoard "1" *-- "0..n" ChessPiece : contains
    ChessPiece <|-- KingPiece
    ChessPiece <|-- HorsePiece

上述 UML 类图展示了 ChessPiece ChessBoard 的关系及继承结构,体现了组合与多态的设计思想。

3.2 继承机制实现不同棋子的行为差异

虽然所有棋子共享一些共性(颜色、位置、存活状态),但其移动规则千差万别。利用继承机制,可以在保持接口一致的前提下,为每种兵种定制专属行为。

3.2.1 基类与派生类的层级结构设计

3.2.1.1 定义抽象基类 MoveRuleInterface

为更精细地分离关注点,可进一步提取移动规则接口:

class MoveRuleInterface {
public:
    virtual ~MoveRuleInterface() = default;
    virtual bool canMove(const ChessBoard& board, int fromX, int fromY, int toX, int toY) const = 0;
};

然后让各棋子类组合此接口,而非直接继承 ChessPiece 。这种方式称为“策略模式”,更适合规则复杂的场景。

但出于简洁考虑,仍采用经典继承结构,以 ChessPiece 为抽象基类。

3.2.1.2 各兵种(将、士、象、马、车、炮、兵)的继承实现

以“马”为例,其实现需满足“日字走法”且“不可蹩腿”。

class HorsePiece : public ChessPiece {
public:
    HorsePiece(PlayerColor c, int x, int y, int id) : ChessPiece(c, PieceType::HORSE, x, y, id) {}

    bool isValidMove(int toX, int toY, const ChessBoard& board) const override {
        int dx = abs(toX - getX());
        int dy = abs(toY - getY());

        // 马走“日”:形成 2x1 或 1x2 的矩形
        if (!((dx == 2 && dy == 1) || (dx == 1 && dy == 2))) {
            return false;
        }

        // 检查蹩腿:中间的“拐脚”位置是否有棋子
        int centerX = (toX + getX()) / 2;
        int centerY = (toY + getY()) / 2;

        if (dx == 2) {
            centerX = getX() + (toX > getX() ? 1 : -1);  // 横向跳,检查垂直方向挡路
            return board.isEmpty(centerX, getY());
        } else {
            centerY = getY() + (toY > getY() ? 1 : -1);  // 纵向跳,检查水平方向挡路
            return board.isEmpty(getX(), centerY);
        }
    }

    std::vector<std::pair<int, int>> getValidMoves(const ChessBoard& board) const override {
        std::vector<std::pair<int, int>> moves;
        int directions[8][2] = {{2,1},{2,-1},{-2,1},{-2,-1},
                                {1,2},{1,-2},{-1,2},{-1,-2}};
        for (auto& d : directions) {
            int nx = getX() + d[0];
            int ny = getY() + d[1];
            if (nx >= 0 && nx < 10 && ny >= 0 && ny < 9) {
                if (isValidMove(nx, ny, board)) {
                    moves.emplace_back(nx, ny);
                }
            }
        }
        return moves;
    }
};

参数说明与逻辑分析:
- dx , dy :计算位移绝对值,判断是否构成“日”字;
- centerX , centerY :根据跳跃方向确定“蹩腿”检测点;
- board.isEmpty(...) :调用棋盘接口检测阻挡;
- getValidMoves() :预生成所有可能落点,供AI评估使用。

其他兵种类似实现,如“炮”的吃子需跨越一个障碍,“象”不能过河等。

3.2.2 多态调用在走法判断中的应用

3.2.2.1 统一接口下的差异化移动验证

当用户尝试移动某棋子时,系统只需调用其 isValidMove() 方法,无需关心具体类型:

bool ChessBoard::movePiece(int fromX, int fromY, int toX, int toY) {
    ChessPiece* piece = getPieceAt(fromX, fromY);
    if (!piece || !piece->isValidMove(toX, toY, *this)) {
        return false;  // 移动非法
    }

    // 执行移动逻辑
    if (!isEmpty(toX, toY)) {
        removePieceAt(toX, toY);  // 吃子
    }
    piece->setPosition(toX, toY);
    grid[toX][toY] = std::move(grid[fromX][fromY]);
    grid[fromX][fromY] = nullptr;

    return true;
}

由于 piece->isValidMove() 是虚函数调用,实际执行的是对应派生类的实现,实现了运行时多态。

3.2.2.2 运行时绑定提升扩展性与灵活性

这种设计的优势在于:
- 新增兵种只需添加新类并重写 isValidMove
- 规则变更不影响调用方代码;
- 支持动态加载插件式兵种(配合工厂模式);

sequenceDiagram
    participant User
    participant GameLogic
    participant ChessBoard
    participant ChessPiece

    User->>GameLogic: 输入移动指令 (e2,e4)
    GameLogic->>ChessBoard: movePiece(e2,e4)
    ChessBoard->>ChessPiece: getPieceAt(e2)
    ChessPiece-->>ChessBoard: 返回 HorsePiece*
    ChessBoard->>HorsePiece: isValidMove(e4)
    alt 合法
        HorsePiece-->>ChessBoard: true
        ChessBoard->>ChessBoard: 执行移动
        ChessBoard-->>GameLogic: success
    else 不合法
        HorsePiece-->>ChessBoard: false
        ChessBoard-->>GameLogic: fail
    end

该序列图展示了多态调用的实际流程,强调了运行时动态绑定的关键作用。

3.3 封装性与信息隐藏原则的实际体现

良好的封装不仅能防止误操作,还能降低模块间依赖,提高测试便利性。

3.3.1 私有成员保护核心数据不被非法访问

ChessBoard 中, grid allPieces 被设为 private ,外界只能通过 getPieceAt() 等受控接口访问。这样可以防止外部代码绕过规则直接修改棋盘状态。

例如,禁止如下非法操作:

// ❌ 错误示范:直接修改 grid
chessBoard.grid[0][0].reset();  // 编译失败!private 成员不可见

必须通过公共方法间接操作:

// ✅ 正确方式:通过接口删除
chessBoard.removePieceAt(0, 0);

同时可在 removePieceAt 内部加入日志记录、事件广播等副作用控制。

3.3.2 公共接口提供安全的操作入口点

所有修改操作都应经过校验。例如, movePiece 应拒绝无效输入:

bool ChessBoard::movePiece(int fromX, int fromY, int toX, int toY) {
    // 边界检查
    if (fromX < 0 || fromX >= BOARD_ROWS || fromY < 0 || fromY >= BOARD_COLS ||
        toX < 0 || toX >= BOARD_ROWS || toY < 0 || toY >= BOARD_COLS) {
        return false;
    }

    ChessPiece* piece = getPieceAt(fromX, fromY);
    if (!piece || !piece->isAlive()) return false;

    if (!piece->isValidMove(toX, toY, *this)) return false;

    // ... 执行移动 ...
}

通过这一系列前置检查,确保只有合法请求才能改变状态,极大增强了系统的鲁棒性。

接口方法 访问级别 功能描述
getPieceAt() public 查询某位置棋子(只读)
movePiece() public 封装完整移动流程
setupInitialPosition() public 初始化布阵
grid private 实际存储结构,禁止直访
validateMoveContext() protected 内部辅助函数,供派生类复用

综上所述,通过严格的封装设计, ChessBoard 成为一个可信的“权威来源”,所有状态变更均需经过审核,杜绝了脏数据传播的风险。

4. 中国象棋规则实现的核心逻辑

中国象棋作为一项具有深厚文化底蕴的智力竞技项目,其规则体系不仅体现了中国古代哲学中的阴阳平衡与兵法谋略,也对现代软件工程中状态建模、行为抽象和逻辑判断提出了高要求。在开发一个可运行的象棋程序时, 将真实世界的游戏规则转化为计算机可执行的形式化逻辑 是核心挑战之一。本章深入剖析中国象棋各项关键规则的技术实现路径,重点聚焦于棋子移动机制的形式化表达以及特殊游戏状态(如“将军”、“将死”)的判定策略。这些规则不仅是游戏合法性的基石,也是后续走法生成、AI评估函数设计的基础支撑。

4.1 棋子移动规则的形式化表达

在中国象棋中,不同兵种遵循截然不同的移动模式,这种多样性决定了必须为每类棋子单独建模其运动轨迹,并结合当前棋盘环境进行动态验证。形式化表达的核心在于:将自然语言描述的规则转换为结构化的数据与算法流程,确保每一个潜在走法都能被精确计算并验证。为此,我们采用“方向向量+步长限制+障碍检测”的通用框架来统一处理各类棋子的移动逻辑,同时保留各兵种特有的约束条件。

4.1.1 将(帅)的活动范围与限制条件

将(红方称“帅”,黑方称“将”)是中国象棋中最受保护的棋子,其行动受到严格的空间限制。根据规则,将只能在己方的“九宫格”内活动,即横坐标为3~5(列D~F),纵坐标对于红方为0~2,黑方为7~9。此外,两将不能在同一直线上直接相对而无遮挡——这被称为“飞将”或“照面”规则。

为了高效实现这一规则,我们可以预先定义九宫格的有效坐标集,并通过边界检查函数快速过滤非法位置:

struct Position {
    int row, col;
};

const std::vector<Position> GENERAL_AREA_RED = {
    {0,3}, {0,4}, {0,5},
    {1,3}, {1,4}, {1,5},
    {2,3}, {2,4}, {2,5}
};

const std::vector<Position> GENERAL_AREA_BLACK = {
    {7,3}, {7,4}, {7,5},
    {8,3}, {8,4}, {8,5},
    {9,3}, {9,4}, {9,5}
};

bool isInPalace(int row, int col, bool isRed) {
    const auto& palace = isRed ? GENERAL_AREA_RED : GENERAL_AREA_BLACK;
    return std::find(palace.begin(), palace.end(), Position{row, col}) != palace.end();
}

代码逻辑逐行解读:

  • 第1–2行:定义 Position 结构体用于封装行列坐标。
  • 第4–16行:使用 std::vector<Position> 静态初始化红黑双方九宫格的所有合法坐标点。这种方式优于简单的行列区间判断,因为它可以扩展支持非矩形区域(例如未来变体棋)。
  • 第18–21行: isInPalace 函数接收当前位置及阵营信息,查询该位置是否属于对应九宫格。时间复杂度为O(1),因集合大小固定且仅9个元素,适合线性查找。

进一步地,“飞将”规则需在每次移动后检测两条将之间是否有其他棋子阻挡:

bool canFlyGeneral(const ChessBoard& board, const Position& redGen, const Position& blackGen) {
    if (redGen.col != blackGen.col) return false; // 不在同一列
    int count = 0;
    for (int r = redGen.row + 1; r < blackGen.row; ++r) {
        if (board.getPiece(r, redGen.col)) count++;
    }
    return count == 0; // 无人阻挡则构成“飞将”
}

此函数可用于判断是否发生违规暴露或将处于被照面状态。实际游戏中应禁止导致飞将的走法。

表格:将(帅)移动合法性判定要素汇总
判定维度 合法条件 技术实现方式
坐标范围 必须位于己方九宫格 查表匹配预设坐标集合
移动步长 每次仅允许上下左右移动一格 曼哈顿距离等于1且方向合法
飞将检测 不能与对方将在同列且中间无遮挡 扫描列间棋子数量
目标位置 可吃敌子但不可自相残 目标位为空或为敌方非将棋

上述机制保证了将的行为完全符合规则,同时也为后续“将军”检测提供了基础接口。

4.1.2 马走“日”字与蹩腿检测算法

马走“日”字是中国象棋最具特色的移动方式之一,表现为从原点出发沿L型移动两格再转一格(或反之)。然而,马存在“蹩腿”现象:若其前进方向的第一步有棋子阻挡,则无法完成跳跃。

形式化建模如下:

  • 方向向量集 :马有8个可能跳向,对应偏移量:
    cpp const std::vector<std::pair<int, int>> KNIGHT_MOVES = { {-2, -1}, {-2, 1}, {-1, -2}, {-1, 2}, {1, -2}, {1, 2}, {2, -1}, {2, 1} };

  • 蹩腿点映射 :每个跳跃方向对应一个“绊马腿”位置,例如向左上跳两行一列时,绊点为 (row -1, col)

由此可建立方向到绊点的映射关系:

struct KnightMove {
    int dr, dc;
    int br, bc; // 绊马腿相对坐标
};

const std::vector<KnightMove> KNIGHT_JUMP_TABLE = {
    {-2, -1, -1, -1}, // 上左
    {-2, 1, -1, 1},  // 上右
    {-1, -2, -1, -1}, // 左上
    {-1, 2, -1, 2},   // 右上
    {1, -2, 1, -1},   // 左下
    {1, 2, 1, 2},     // 右下
    {2, -1, 1, -1},   // 下左
    {2, 1, 1, 1}      // 下右
};

基于此表,完整移动验证函数如下:

bool isValidKnightMove(const ChessBoard& board, int sr, int sc, int tr, int tc) {
    for (const auto& move : KNIGHT_JUMP_TABLE) {
        int nr = sr + move.dr;
        int nc = sc + move.dc;
        if (nr == tr && nc == tc) {
            int br = sr + move.br;
            int bc = sc + (move.dc == ±2 ? move.bc : move.bc); // 精确定位绊点
            return !board.getPiece(br, bc); // 绊点无子方可跳
        }
    }
    return false;
}

参数说明:
- sr , sc : 起始行、列
- tr , tc : 目标行、列
- 函数返回布尔值表示是否为合法马步

该设计将几何变换与障碍检测分离,提高了可读性和维护性。

Mermaid 流程图:马走法合法性判断流程
graph TD
    A[开始] --> B{目标位置是否符合8个日字方向?}
    B -- 否 --> C[非法移动]
    B -- 是 --> D[确定对应的绊马腿坐标]
    D --> E{绊马腿位置是否有棋子?}
    E -- 有 --> F[移动失败: 蹩腿]
    E -- 无 --> G[移动成功]

此流程清晰表达了马移动判断的关键分支逻辑,适用于单元测试用例设计。

4.1.3 象飞“田”字与塞象眼判断

象(相)的移动为“田”字对角跳动,即移动两格行加两格列,且不能过河(红不过河界第5行,黑不超第4行)。其特殊之处在于“塞象眼”:若中心交叉点(象眼)被占据,则无法飞行。

实现思路类似马,但方向更少(仅4个对角方向),且需额外添加阵营越界判断:

const std::vector<std::tuple<int,int,int,int>> ELEPHANT_MOVES = {
    {-2,-2, -1,-1}, // 左上
    {-2,2, -1,1},   // 右上
    {2,-2, 1,-1},   // 左下
    {2,2, 1,1}      // 右下
};

bool isValidElephantMove(bool isRed, int sr, int sc, int tr, int tc, const ChessBoard& board) {
    for (const auto& [dr, dc, er, ec] : ELEPHANT_MOVES) {
        if (sr + dr == tr && sc + dc == tc) {
            int midR = sr + er;
            int midC = sc + ec;
            if (board.getPiece(midR, midC)) return false; // 象眼堵塞
            int finalRow = sr + dr;
            if (isRed && finalRow < 5) continue; // 红象不过河
            if (!isRed && finalRow > 4) continue; // 黑象不过河
            return true;
        }
    }
    return false;
}

该函数综合了路径阻断与地理边界双重限制,确保象的移动完全合规。

4.1.4 炮的吃子与平移机制解析

炮的移动机制独特:移动时不吃子(类似车),吃子时必须隔一子(“炮架”)。因此其实现分为两个阶段:常规移动与攻击移动。

bool isValidCannonMove(const ChessBoard& board, int sr, int sc, int tr, int tc, bool isCapture) {
    if (sr != tr && sc != tc) return false; // 必须直线移动

    int stepR = (tr > sr) ? 1 : (tr < sr) ? -1 : 0;
    int stepC = (tc > sc) ? 1 : (tc < sc) ? -1 : 0;

    int r = sr + stepR, c = sc + stepC;
    int hurdleCount = 0;

    while (r != tr || c != tc) {
        if (board.getPiece(r, c)) hurdleCount++;
        r += stepR; c += stepC;
    }

    if (!isCapture) return hurdleCount == 0; // 移动途中无子
    else {
        Piece* target = board.getPiece(tr, tc);
        return hurdleCount == 1 && target && target->isEnemyOf(attacker);
    }
}

逻辑分析:
- 第1–3行:非直线移动直接拒绝
- 第5–7行:计算步进方向
- 第9–15行:遍历路径统计中途棋子数
- 第17–19行:非吃子要求无障碍;吃子要求恰好一个“炮架”

此算法准确模拟了炮的独特作战方式,是规则系统中最复杂的案例之一。

4.2 特殊规则的状态判定

除基本移动外,中国象棋包含若干决定胜负走向的关键状态,如“将军”、“将死”、“长将”等。这些状态涉及全局棋局感知能力,必须通过反向推理与组合搜索才能正确识别。

4.2.1 “将军”状态的检测方法

“将军”指一方的将正受到对方棋子的直接威胁。检测方法是对当前被检方的将所在位置,反向扫描所有敌方棋子能否攻击到该点。

bool isKingInCheck(const ChessBoard& board, bool kingIsRed) {
    Position genPos = board.findGeneral(kingIsRed);
    for (int r = 0; r < 10; ++r) {
        for (int c = 0; c < 9; ++c) {
            Piece* p = board.getPiece(r, c);
            if (p && p->isRed() != kingIsRed) { // 敌方棋子
                if (p->canAttack(board, r, c, genPos.row, genPos.col)) {
                    return true;
                }
            }
        }
    }
    return false;
}

该函数依赖于每个棋子类实现 canAttack 虚函数,体现多态优势。

改进方案:攻击路径缓存优化

为避免重复全盘扫描,可引入“攻击图”(attack map)机制,记录每个位置被哪些势力控制:

class AttackMap {
public:
    void update(const ChessBoard& board);
    bool isAttacked(int row, int col, bool byRed) const;
private:
    uint8_t data[10][9]; // 位标志:bit0=红控,bit1=黑控
};

每次走子后更新受影响区域,大幅提升性能。

4.2.2 “将死”局面的判定逻辑

“将死”是指将处于被将军状态,且所有可能的逃脱方式均被封堵。判定需三步:

  1. 当前处于“将军”状态;
  2. 尝试所有己方可动棋子的所有合法走法;
  3. 若无任何走法能解除将军,则为将死。
bool isCheckmated(const ChessBoard& board, bool defenderIsRed) {
    if (!isKingInCheck(board, defenderIsRed)) return false;

    std::vector<Move> allMoves = generateLegalMoves(board, defenderIsRed);
    for (const Move& m : allMoves) {
        ChessBoard temp = board;
        temp.makeMove(m);
        if (!isKingInCheck(temp, defenderIsRed)) {
            return false; // 存在解将走法
        }
    }
    return true;
}

该过程本质是浅层博弈树展开,时间复杂度较高,但必要。

4.2.3 “长将”、“困毙”与和棋判定

四级子节详解

“长将”指一方连续不断地将军,迫使对方无法正常应对。依据规则,若同一局面出现三次,或连续六十回合无吃子无兵动,则判和。

为此需引入:

  • 局面哈希器 :使用Zobrist Hashing生成唯一标识
  • 计数器管理器 :记录重复次数与静默回合
class GameRuleMonitor {
    std::map<uint64_t, int> positionCount;
    int quietTurns; // 无进展回合
public:
    void onMove(const ChessBoard& board, bool isCapture, bool isPawnMove);
    bool shouldDeclareDraw() const;
};

每当走子后调用 onMove ,更新哈希计数。若任一哈希值≥3,或 quietTurns >= 60 ,触发和棋。

此外,“困毙”指轮到一方走棋时无合法走法且未被将军,属特殊和棋情形:

bool isStalemate(const ChessBoard& board, bool playerToMove) {
    if (isKingInCheck(board, playerToMove)) return false;
    return generateLegalMoves(board, playerToMove).empty();
}

此类状态虽少见,但在AI对抗中频繁出现,必须妥善处理。

Mermaid 图:特殊状态判定决策流
graph LR
    A[当前玩家走棋] --> B{是否有合法走法?}
    B -- 否 --> C{是否被将军?}
    C -- 是 --> D[将死 → 对方胜]
    C -- 否 --> E[困毙 → 和棋]
    B -- 是 --> F{是否连续三次相同局面?}
    F -- 是 --> G[长将/重复局面 → 和棋]
    F -- 否 --> H[继续游戏]

该图整合多种终止条件,指导主循环做出裁决。

表格:特殊规则判定指标对比
规则类型 触发条件 检测频率 数据结构依赖
将军 将位于敌方可攻位置 每步后 攻击可达性
将死 被将军且无可走之步 每步后 合法走法生成器
困毙 未被将军但无合法走法 每步后 同上
长将 连续多次将军导致循环 异步监测 局面哈希+计数器
重复局面 Zobrist哈希出现三次 每步后 哈希表
自然限招 60回合内无吃子或兵移动 计数器 静默回合累加器

综上所述,中国象棋规则的数字化实现是一个多层次、跨模块的系统工程。它要求开发者既精通规则细节,又能运用面向对象、算法优化和状态机等多种编程范式协同解决复杂问题。唯有如此,方能构建出既忠实于传统又具备现代软件特质的高质量象棋引擎。

5. 合法走法检测算法的设计与实现

在构建一个完整且可靠的中国象棋游戏系统时, 合法走法检测 是连接用户输入与游戏逻辑的核心枢纽。它不仅决定了玩家是否可以执行某次移动,更深层次地影响着整个系统的健壮性、响应速度以及用户体验。尤其在引入AI对弈或网络联机功能后,该模块的准确性与效率将直接决定系统能否稳定运行。本章节聚焦于从图搜索算法的应用出发,建立一套分层过滤的合法性验证机制,并在此基础上进行性能评估与剪枝优化,最终形成可扩展、高内聚、低耦合的走法检测架构。

5.1 图搜索算法在走法生成中的应用

在中国象棋中,每个棋子具有独特的移动方式,这些规则本质上是对状态空间的一种遍历约束。为了系统化地生成所有可能的合法位置,图搜索算法提供了一种通用而强大的建模工具。通过将棋盘视为图结构,格点为节点,可行移动路径为边,我们可以利用深度优先搜索(DFS)、广度优先搜索(BFS)甚至启发式A*算法来探索潜在的走法集合。这种方法特别适用于需要动态生成走法的情景,如AI评估函数设计或复杂局面推演。

5.1.1 深度优先搜索(DFS)用于递归试探

深度优先搜索是一种基于栈的遍历策略,适合用于试探性路径查找。在象棋中,DFS可用于模拟“尝试一步—判断结果—回溯”的过程,常用于解决是否存在某种解的问题,例如判定是否可以通过一系列走法解除“将军”状态。

// DFS示例:试探某个棋子从起点(sx, sy)到目标(tx, ty)是否可达
bool dfs(int sx, int sy, int tx, int ty, vector<vector<bool>>& visited, ChessBoard& board) {
    // 边界检查与目标匹配
    if (sx < 0 || sx >= 9 || sy < 0 || sy >= 10 || visited[sx][sy]) return false;
    if (sx == tx && sy == ty) return true;  // 成功到达目标

    visited[sx][sy] = true;

    // 获取当前棋子的所有基本方向偏移(以车为例)
    int dx[] = {-1, 1, 0, 0};
    int dy[] = {0, 0, -1, 1};

    for (int i = 0; i < 4; ++i) {
        int nx = sx + dx[i], ny = sy + dy[i];
        while (board.isWithinBounds(nx, ny)) {
            if (!board.isEmpty(nx, ny)) {
                if (board.canCapture(sx, sy, nx, ny)) {
                    if (dfs(nx, ny, tx, ty, visited, board)) return true;
                }
                break;  // 遇到障碍停止沿此方向延伸
            }
            if (dfs(nx, ny, tx, ty, visited, board)) return true;
            nx += dx[i]; ny += dy[i];
        }
    }
    return false;
}
代码逻辑逐行解读:
  • visited 数组防止重复访问同一位置,避免无限递归。
  • 函数首先判断坐标越界和是否已访问,若满足终止条件则返回 true
  • 设置当前节点为已访问后,遍历四个基本方向(上下左右),适用于“车”类直线移动棋子。
  • 使用 while 循环实现连续平移(如车的滑动移动),直到遇到非空格子。
  • 若中途能吃子并命中目标,则递归继续;否则跳出该方向搜索。
  • 整体采用回溯思想,在未找到路径时自动退回到上一层调用。

参数说明
- sx , sy : 起始坐标;
- tx , ty : 目标坐标;
- visited : 记录访问状态的二维布尔数组;
- board : 封装了棋盘操作的类实例,提供边界、占位、捕获等接口。

该方法虽直观清晰,但在大规模状态空间中容易导致栈溢出,因此更适合小范围局部搜索或作为辅助验证手段。

5.1.2 广度优先搜索(BFS)遍历可达位置

相较于DFS,广度优先搜索更适合用于 生成某一棋子在当前状态下所有可达的位置集合 。BFS按层次扩展,能够保证首次到达某点即是最短路径,这在需要穷举所有合法落点的场景下尤为关键。

#include <queue>
using namespace std;

struct Position {
    int x, y, steps;
};

vector<Position> bfsGenerateMoves(int start_x, int start_y, ChessPiece* piece, ChessBoard& board) {
    vector<Position> reachable;
    vector<vector<bool>> visited(9, vector<bool>(10, false));
    queue<Position> q;

    q.push({start_x, start_y, 0});
    visited[start_x][start_y] = true;

    int dx[] = {-1, 1, 0, 0, -1, -1, 1, 1};  // 八个方向(示例)
    int dy[] = {0, 0, -1, 1, -1, 1, -1, 1};

    while (!q.empty()) {
        auto cur = q.front(); q.pop();
        reachable.push_back(cur);

        for (int i = 0; i < 8; ++i) {
            int nx = cur.x + dx[i], ny = cur.y + dy[i];
            if (board.isValidMove(piece, cur.x, cur.y, nx, ny) && !visited[nx][ny]) {
                visited[nx][ny] = true;
                q.push({nx, ny, cur.steps + 1});
            }
        }
    }

    return reachable;
}
代码逻辑分析:
  • 使用标准队列实现BFS,确保按距离递增顺序访问节点。
  • 初始化起始点并标记已访问,随后将其入队。
  • 每次取出队首元素,并将其加入结果集 reachable
  • 遍历八个方向,调用 isValidMove() 判断移动合法性(包括越界、阻挡、吃子规则等)。
  • 合法则入队并标记,直至队列为空。

参数说明
- start_x , start_y : 当前棋子所在坐标;
- piece : 棋子对象指针,携带颜色、类型信息;
- board : 棋盘引用,用于调用规则判断函数;
- 返回值为所有可达位置组成的向量。

该方法优点在于全面性和可预测性,但时间开销较大,建议结合具体棋子行为定制方向集,而非统一八向扩展。

表格:DFS与BFS在走法生成中的对比
对比维度 DFS BFS
数据结构 栈(递归或显式栈) 队列
遍历顺序 深入优先 层级扩展
适用场景 存在性判断、路径反推 枚举所有可达点
时间复杂度 O(V + E),V为节点数,E为边数 O(V + E)
空间复杂度 O(h),h为最大深度 O(w),w为最宽层级宽度
是否保证最短路径 是(无权图中)

5.1.3 A*启发式算法优化搜索效率(可选进阶)

对于高级AI引擎而言,盲目搜索成本过高。A*算法通过引入启发函数 $ h(n) $ 来估计从当前节点到目标的代价,结合实际走过路径 $ g(n) $,计算总估值 $ f(n) = g(n) + h(n) $,从而优先探索最有希望的方向。

struct Node {
    int x, y;
    int g, h, f;
    bool operator<(const Node& other) const { return f > other.f; }  // 最小堆比较
};

int heuristic(int x1, int y1, int x2, int y2) {
    return abs(x1 - x2) + abs(y1 - y2);  // 曼哈顿距离
}

bool aStar(int sx, int sy, int tx, int ty, ChessBoard& board) {
    priority_queue<Node> pq;
    vector<vector<int>> dist(9, vector<int>(10, INT_MAX));

    pq.push({sx, sy, 0, heuristic(sx, sy, tx, ty), 0});
    dist[sx][sy] = 0;

    int dx[] = {-1, 1, 0, 0}, dy[] = {0, 0, -1, 1};

    while (!pq.empty()) {
        Node cur = pq.top(); pq.pop();
        if (cur.x == tx && cur.y == ty) return true;

        if (cur.g > dist[cur.x][cur.y]) continue;

        for (int i = 0; i < 4; ++i) {
            int nx = cur.x + dx[i], ny = cur.y + dy[i];
            if (!board.isPassable(nx, ny)) continue;

            int new_g = cur.g + 1;
            if (new_g < dist[nx][ny]) {
                dist[nx][ny] = new_g;
                pq.push({nx, ny, new_g, heuristic(nx, ny, tx, ty), new_g + heuristic(nx, ny, tx, ty)});
            }
        }
    }
    return false;
}
流程图(Mermaid格式)展示A*核心流程:
graph TD
    A[开始 A* 搜索] --> B{当前节点为目标?}
    B -->|是| C[返回成功]
    B -->|否| D[扩展邻居节点]
    D --> E[计算g(n), h(n), f(n)]
    E --> F{f(n) 更优?}
    F -->|是| G[更新距离并加入优先队列]
    F -->|否| H[跳过该节点]
    G --> I{队列为空?}
    H --> I
    I -->|否| B
    I -->|是| J[返回失败]
参数说明与优化建议:
  • heuristic() 函数选择曼哈顿距离是因为象棋棋盘为网格结构,且多数棋子移动受限于正交或对角线。
  • dist[][] 数组记录到达各点的最小步数,用于剪枝冗余路径。
  • 实际应用中,应根据棋子类型调整启发函数权重,例如“马”因走“日”字不可简单使用欧氏距离。

尽管A*在单目标路径规划中表现优异,但由于象棋中每步需考虑全局局势而非单一目标点,其主要用于AI评估阶段的局部最优路径预估,而非日常走法生成。

5.2 合法性验证的分层过滤模型

直接使用图搜索生成所有可能走法仍不足以构成完整的合法性检测体系。真实的走法必须经过多层筛选,排除无效、违规甚至危险的操作。为此,我们提出一种 四层过滤模型 ,逐级收紧判断条件,兼顾安全与效率。

5.2.1 语法合法性:输入是否符合格式

第一层验证关注的是原始输入的文本结构是否合规。无论来自命令行还是GUI界面,用户输入都应遵循统一格式规范,例如“a1-b2”表示从a1移动到b2。

bool validateSyntax(const string& input) {
    if (input.length() != 5) return false;
    if (input[2] != '-') return false;
    char c1 = input[0], r1 = input[1];
    char c2 = input[3], r2 = input[4];

    return (c1 >= 'a' && c1 <= 'i') &&
           (r1 >= '0' && r1 <= '9') &&
           (c2 >= 'a' && c2 <= 'i') &&
           (r2 >= '0' && r2 <= '9');
}

说明 :该函数仅做字符级校验,不涉及语义正确性。后续步骤依赖转换后的坐标进行处理。

5.2.2 物理合法性:是否在棋盘范围内

第二层验证将符号坐标转换为数组索引,并确认其落在有效区域内(横坐标0~8,纵坐标0~9)。

pair<int, int> parsePosition(char col, char row) {
    int x = col - 'a';
    int y = 9 - (row - '0');  // 倒序映射:'0'→9, '1'→8...
    return {x, y};
}

参数说明
- col : 列字母(a-i)对应0-8;
- row : 行数字(0-9)按中国象棋习惯倒置存储;
- 返回数组下标对 (x,y)

结合前一层结果,若任一坐标超出范围,则立即拒绝。

5.2.3 规则合法性:是否违反该棋子移动规则

第三层进入真正的规则判断环节。不同兵种有不同的移动逻辑,可通过多态机制统一调用。

class MoveRuleInterface {
public:
    virtual bool isValidMove(int from_x, int from_y, int to_x, int to_y, const ChessBoard& board) = 0;
};

class HorseRule : public MoveRuleInterface {
public:
    bool isValidMove(int fx, int fy, int tx, int ty, const ChessBoard& board) override {
        int dx = abs(fx - tx), dy = abs(fy - ty);
        if (!((dx == 2 && dy == 1) || (dx == 1 && dy == 2))) return false;
        // 检查蹩腿
        if (dx == 2 && board.isOccupied((fx+tx)/2, fy)) return false;
        if (dy == 2 && board.isOccupied(fx, (fy+ty)/2)) return false;
        return true;
    }
};

逻辑分析
- “马走日”要求横向走两格纵向一格,或反之;
- 中间“蹩腿”位置需检查是否有其他棋子占据;
- (fx+tx)/2 自动取中间点整数坐标。

此类规则封装使得新增兵种只需继承接口即可,无需修改主流程。

5.2.4 战术合法性:是否导致己方处于被将军状态

最后一层也是最关键的一环:即使移动本身合法,若造成“送将”,也应禁止。

bool isMoveTacticallyLegal(int fx, int fy, int tx, int ty, ChessSide side, ChessBoard& board) {
    ChessBoard tempBoard = board;  // 模拟走法
    tempBoard.movePiece(fx, fy, tx, ty);

    return !tempBoard.isGeneralInCheck(side);  // 是否仍被将军
}

说明 :此处复制棋盘状态进行试走,调用第四章所述的“将军检测”算法判断结果。只有当不会导致己方将帅暴露时才允许提交。

四层过滤流程图(Mermaid)
flowchart TB
    A[用户输入] --> B{语法合法?}
    B -->|否| Z[拒绝: 格式错误]
    B -->|是| C{物理坐标有效?}
    C -->|否| Y[拒绝: 越界]
    C -->|是| D{符合棋子移动规则?}
    D -->|否| X[拒绝: 违规走法]
    D -->|是| E{是否导致送将?}
    E -->|是| W[拒绝: 危险操作]
    E -->|否| V[接受: 合法走法]

该模型实现了职责分离,每一层只关心特定问题,便于调试与扩展。

5.3 算法性能评估与剪枝优化

随着棋局深入,尤其是中局阶段,每回合平均可选走法可达数十种,频繁调用DFS/BFS会导致显著延迟。因此必须开展性能剖析,并实施剪枝策略。

5.3.1 时间复杂度分析与热点函数定位

以BFS为例,最坏情况下需访问全部90个格点,每次扩展最多8个方向,故理论时间复杂度为 $ O(720) $,看似不高。但在实际中,由于每步都要调用 isValidMove() ,而该函数内部包含多次函数调用与条件判断,成为性能瓶颈。

使用GProf或Valgrind Callgrind可定位热点:

valgrind --tool=callgrind ./chess-engine
callgrind_annotate callgrind.out.*

常见热点函数包括:
- isGeneralInCheck() —— 被频繁调用用于战术合法性验证;
- getPossibleMoves() —— 在AI搜索树中被反复执行;
- board.copy() —— 模拟走法时深拷贝开销大。

5.3.2 缓存常见走法结果减少重复计算

引入LRU缓存机制,保存最近使用的走法查询结果:

#include <unordered_map>
#include <list>

struct BoardStateHash {
    size_t operator()(const ChessBoard* b) const {
        return hash<string>()(b->getFEN());  // 使用FEN字符串哈希
    }
};

class MoveCache {
private:
    unordered_map<const ChessBoard*, vector<Move>, BoardStateHash> cache;
    list<const ChessBoard*> lru;
    static const int MAX_SIZE = 1000;

public:
    bool get(const ChessBoard* board, vector<Move>& moves) {
        auto it = cache.find(board);
        if (it != cache.end()) {
            // 移至LRU前端
            lru.remove(board); lru.push_front(board);
            moves = it->second;
            return true;
        }
        return false;
    }

    void put(const ChessBoard* board, const vector<Move>& moves) {
        if (cache.size() >= MAX_SIZE) {
            const ChessBoard* old = lru.back(); lru.pop_back();
            cache.erase(old);
        }
        cache[board] = moves;
        lru.push_front(board);
    }
};

优势 :在相同局面重复出现时(如打谱复盘、长将检测),可直接命中缓存,避免重算。

结合Zobrist哈希可进一步提升键值唯一性与查找效率。

综上,合法走法检测不仅是规则的翻译器,更是系统性能与安全的守门人。通过图搜索建模、分层过滤与智能缓存,我们构建了一个兼具准确性与高效性的核心模块,为后续AI与多人交互奠定坚实基础。

6. 游戏逻辑控制与状态机设计

在构建一个完整且可扩展的中国象棋程序时,仅仅实现棋子走法和规则判断是远远不够的。要使整个系统具备清晰的行为流程、稳定的运行节奏以及良好的用户体验,必须引入 游戏逻辑控制机制 状态机模型 。本章将深入探讨如何通过结构化的主循环架构、状态驱动的设计模式以及回合制控制策略,来组织和协调复杂的交互行为。

6.1 游戏主循环架构设计

现代游戏引擎或交互式应用程序的核心通常围绕“主循环”(Main Loop)展开。该循环持续监听用户输入、更新内部状态并刷新显示画面,构成了整个系统的生命线。对于象棋这类回合制策略游戏而言,虽然无需实时渲染动画帧,但主循环依然承担着事件响应、状态推进和逻辑调度的关键职责。

6.1.1 初始化→运行→终止的生命周期管理

任何程序都遵循一定的生命周期轨迹,象棋游戏也不例外。从启动到退出,其过程可分为三个典型阶段:初始化(Initialization)、运行(Running)和终止(Termination)。每个阶段都有明确的任务边界与资源管理策略。

  • 初始化阶段 负责加载必要的数据结构,如创建棋盘对象、初始化所有棋子位置、设定玩家信息,并准备图形界面或命令行交互环境。
  • 运行阶段 则是主循环不断执行的核心部分,它按周期性步骤处理输入、验证动作、更新状态、检测胜负。
  • 终止阶段 涉及资源释放、日志记录、保存对局结果等收尾工作。

这一流程可以用如下 Mermaid 流程图表示:

graph TD
    A[程序启动] --> B{是否成功初始化?}
    B -- 是 --> C[进入主循环]
    B -- 否 --> D[报错并退出]
    C --> E[处理用户输入]
    E --> F[更新游戏状态]
    F --> G[检查游戏是否结束]
    G -- 否 --> C
    G -- 是 --> H[触发终止逻辑]
    H --> I[释放资源, 退出程序]

这种分层式的生命周期管理不仅提高了代码的可读性,也便于后期调试和功能扩展。例如,在初始化失败时可以快速定位问题模块;在终止阶段统一回收内存资源,避免泄漏。

主循环伪代码实现

下面是一个典型的主循环结构示例:

enum GameState { INIT, RUNNING, PAUSED, GAME_OVER };

class ChessGame {
private:
    GameState currentState;
    ChessBoard board;
    Player currentPlayer;

public:
    void initialize() {
        currentState = INIT;
        board.setupInitialPosition(); // 设置初始布局
        currentPlayer = RED_PLAYER;
        currentState = RUNNING; // 进入运行状态
        std::cout << "游戏已初始化,红方先行。\n";
    }

    void mainLoop() {
        while (currentState != GAME_OVER) {
            switch (currentState) {
                case RUNNING:
                    handleInput();
                    updateState();
                    render();
                    break;
                case PAUSED:
                    showPauseMenu();
                    break;
                default:
                    break;
            }
            std::this_thread::sleep_for(std::chrono::milliseconds(50)); // 控制帧率
        }
        shutdown();
    }

    void shutdown() {
        std::cout << "游戏结束,正在清理资源...\n";
        // 可添加文件保存、统计输出等功能
    }
};

代码逻辑逐行解读分析:

  • 第3行定义了 GameState 枚举类型,用于标记当前所处的游戏阶段,这是状态机的基础。
  • initialize() 方法中调用了 setupInitialPosition() 来布子,完成后将状态切换为 RUNNING ,确保只有成功初始化后才开始主循环。
  • mainLoop() 使用 while 循环持续运行,直到状态变为 GAME_OVER 才退出。
  • 内部使用 switch-case 根据当前状态决定执行路径,体现了状态驱动的思想。
  • 每次循环末尾加入 sleep_for 延迟,防止 CPU 占用过高,适用于非图形界面场景下的节流控制。

该设计具有高度模块化特征,易于集成 GUI 框架或网络通信模块。

6.1.2 帧更新与事件驱动的协调机制

尽管象棋不是动作类游戏,但仍需考虑“帧”的概念。这里的“帧”并非图像刷新率意义上的帧,而是指主循环中的最小时间单位,即一次完整的输入-处理-输出周期。

两种常见的更新方式对比
更新模式 特点 适用场景
固定帧率更新 每隔固定毫秒执行一次循环 简单稳定,适合本地单机版
事件驱动更新 仅当有输入或状态变化时才触发 节省资源,适合网络/移动端
混合模式 结合两者优点,空闲时休眠,事件到来立即响应 推荐方案

推荐采用混合模式以平衡性能与响应速度。例如,在无操作期间让主线程短暂休眠,一旦捕获鼠标点击或键盘指令则立即唤醒处理。

输入事件处理流程表
步骤 动作描述 示例
1 监听输入设备 鼠标点击坐标 (x,y)
2 映射物理坐标至棋盘索引 (x,y) → row=7, col=3
3 判断是否选中己方棋子 当前玩家为红方,检测该格是否有红子
4 若已选子,则尝试移动 发起 move(from, to) 请求
5 触发合法性校验链 调用 MoveValidator 验证走法
6 成功则更新棋盘,切换玩家 board.movePiece(…); togglePlayer()

上述流程可通过回调函数机制解耦。例如定义如下接口:

typedef std::function<void(const Move&)> OnMoveCallback;

class InputHandler {
public:
    void setOnMoveListener(OnMoveCallback cb) {
        onMoveCallback = cb;
    }

    void processClick(int screenX, int screenY) {
        Position pos = screenToBoard(screenX, screenY);
        if (isValidPosition(pos)) {
            selectedPos = pos;
            if (onMoveCallback)
                onMoveCallback(Move(lastSelected, pos));
        }
    }
private:
    OnMoveCallback onMoveCallback;
    Position selectedPos;
};

参数说明与扩展性分析:

  • setOnMoveListener() 接受一个 std::function 类型的回调,允许外部注入处理逻辑(如AI决策、网络同步),实现关注点分离。
  • processClick() 将屏幕坐标转换为棋盘坐标后,生成 Move 对象并通过回调通知上层模块。
  • 此设计支持未来接入不同前端框架(如SFML、Qt),只需更换 screenToBoard() 的实现即可。

综上所述,合理的主循环架构不仅能保证程序流畅运行,还能为后续功能扩展打下坚实基础。

6.2 状态机模型实现游戏阶段切换

为了更精确地管理复杂的游戏行为流,应引入有限状态机(Finite State Machine, FSM)作为核心控制模型。状态机能够清晰表达各个阶段之间的转换关系,有效减少条件嵌套带来的混乱。

6.2.1 定义游戏状态枚举:开始、对弈、暂停、结束

首先,我们定义一组标准状态,涵盖常见游戏阶段:

enum class GamePhase {
    START_MENU,     // 主菜单
    IN_PROGRESS,    // 正常对弈中
    PAUSED,         // 暂停状态
    CHECKMATE,      // 将死,游戏结束
    STALEMATE,      // 困毙/和棋
    RESIGN,         // 一方认输
    DRAW_REQUESTED  // 和棋请求中
};

这些状态覆盖了从开局到终局的所有关键节点,有助于在UI层面进行精准反馈。

状态迁移表格示例
当前状态 触发事件 下一状态 条件说明
START_MENU 用户点击“开始游戏” IN_PROGRESS 初始化棋盘
IN_PROGRESS 检测到“将军” IN_PROGRESS 提示危险,不改变状态
IN_PROGRESS 某方无法应将 CHECKMATE 胜负已分
IN_PROGRESS 双方同意和棋 STALEMATE 自动判定
IN_PROGRESS 用户按下Esc键 PAUSED 弹出暂停菜单
PAUSED 用户选择“继续” IN_PROGRESS 返回对局
PAUSED 用户选择“退出” START_MENU 返回主界面
CHECKMATE 用户确认结果 START_MENU 显示胜利者

此表可用于指导状态转移逻辑的编写,避免遗漏边缘情况。

6.2.2 状态转移条件与触发事件绑定

状态转移不应由硬编码的 if-else 实现,而应通过事件订阅机制动态绑定。以下是基于观察者模式的状态机骨架:

class StateMachine {
private:
    GamePhase currentState;
    std::map<GamePhase, std::function<void()>> onEnterActions;
    std::map<std::pair<GamePhase, GameEvent>, GamePhase> transitions;

public:
    void defineTransition(GamePhase from, GameEvent event, GamePhase to) {
        transitions[{from, event}] = to;
    }

    void fireEvent(GameEvent event) {
        auto key = std::make_pair(currentState, event);
        if (transitions.find(key) != transitions.end()) {
            GamePhase oldState = currentState;
            currentState = transitions[key];
            onStateExit(oldState);
            onStateEnter(currentState);
        }
    }

    GamePhase getCurrentState() const { return currentState; }
};

代码逻辑解析:

  • 使用 std::map 存储状态转移规则,键为 (from_state, event) 组合,值为目标状态。
  • fireEvent() 方法模拟事件触发,查找是否存在匹配的转移路径。
  • onStateEnter/Exit 可注册进入或离开某状态时的副作用操作(如播放音效、更新UI)。

这样做的优势在于:
- 支持运行时动态修改规则;
- 易于测试和可视化状态流转;
- 符合开闭原则,新增状态不影响原有代码。

6.2.3 状态行为封装避免逻辑耦合

为防止状态相关逻辑散落在各处,建议为每种状态定义独立的行为类。例如:

class GameState {
public:
    virtual ~GameState() = default;
    virtual void enter() = 0;
    virtual void exit() = 0;
    virtual void handleInput() = 0;
    virtual void update() = 0;
};

class InProgressState : public GameState {
public:
    void enter() override {
        std::cout << "进入对弈状态\n";
    }

    void handleInput() override {
        // 处理落子、悔棋等操作
    }

    void update() override {
        if (board.isCheckmate()) {
            stateMachine->fireEvent(GAME_OVER);
        }
    }
};

设计思想延伸:

  • 每个状态继承自抽象基类 GameState ,强制实现统一接口。
  • update() 中进行状态内逻辑判断,符合条件时触发事件,由状态机接管转移。
  • 这种方式实现了 高内聚低耦合 ,有利于团队协作开发。

6.3 回合制控制与玩家交替机制

中国象棋是典型的回合制游戏,双方轮流行动。因此必须维护当前玩家标识,并在合法移动后自动切换。

6.3.1 当前玩家标识维护与切换逻辑

最简单的实现是使用布尔变量或枚举:

enum Player { RED, BLACK };

class TurnManager {
private:
    Player currentPlayer;

public:
    TurnManager() : currentPlayer(RED) {}

    Player getCurrentPlayer() const { return currentPlayer; }

    void nextTurn() {
        currentPlayer = (currentPlayer == RED) ? BLACK : RED;
        std::cout << "轮到" << (currentPlayer == RED ? "红方" : "黑方") << "走子\n";
    }

    bool isRedsTurn() const { return currentPlayer == RED; }
};

参数说明:

  • nextTurn() 通过三元运算符完成颜色切换,简洁高效。
  • 输出提示语增强交互体验,也可替换为信号通知UI更新。

进一步优化可结合状态机,在每次 movePiece() 成功后自动调用 nextTurn()

6.3.2 超时处理与强制走子策略(适用于AI或网络模式预留接口)

在网络对战或带计时器的比赛中,需引入超时机制。以下是一个简化的倒计时控制器:

class Timer {
private:
    std::chrono::steady_clock::time_point startTime;
    int timeLeftSeconds;
    bool running;

public:
    Timer(int seconds) : timeLeftSeconds(seconds), running(false) {}

    void start() {
        running = true;
        startTime = std::chrono::steady_clock::now();
    }

    void stop() { running = false; }

    bool isExpired() {
        if (!running) return false;
        auto now = std::chrono::steady_clock::now();
        auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(now - startTime);
        return elapsed.count() >= timeLeftSeconds;
    }

    int getTimeLeft() {
        auto now = std::chrono::steady_clock::now();
        auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(now - startTime);
        return std::max(0, timeLeftSeconds - (int)elapsed.count());
    }
};

应用场景说明:

  • IN_PROGRESS 状态中为每位玩家配置独立计时器。
  • 每次切换回合时重置对方计时器并启动新的倒计时。
  • 主循环中定期调用 isExpired() 检查是否超时,若超时则视为弃权,触发 RESIGN 状态。

此外,还可预留 AI 接口:

class AIPlayer {
public:
    virtual Move suggestMove(const ChessBoard& board, Player player) = 0;
};

// 在轮到AI时调用:
if (currentPlayer == BLACK && aiEnabled) {
    Move m = aiPlayer->suggestMove(board, BLACK);
    board.applyMove(m);
    turnManager.nextTurn();
}

这为未来集成搜索算法(如Alpha-Beta剪枝)提供了良好扩展性。

综上所述,游戏逻辑控制不仅是技术实现的终点,更是连接规则层与表现层的桥梁。通过精心设计的主循环、状态机与回合管理系统,可以使象棋程序具备工业级的稳定性与可维护性。

7. 用户交互、持久化与项目工程化实践

7.1 用户输入处理与错误验证机制

在象棋程序中,用户输入是驱动游戏运行的关键入口。无论是命令行界面(CLI)还是图形界面(GUI),都必须对用户的操作进行精准解析和合法性校验。

7.1.1 命令行输入解析与格式校验

以文本输入为例,玩家通常通过坐标表示走法,如 "e2e4" 表示从 (4,2) 移动到 (4,4) (假设采用A~H为列,1~9为行的代数记谱法)。我们可以设计一个解析函数:

#include <regex>
#include <string>

bool parseMoveInput(const std::string& input, int& fromX, int& fromY, int& toX, int& toY) {
    // 正则匹配 e2e4 格式
    std::regex pattern("([a-h])([1-9])([a-h])([1-9])");
    std::smatch matches;

    if (!std::regex_match(input, matches, pattern)) {
        return false; // 格式不合法
    }

    fromX = matches[1].str()[0] - 'a';      // a-h → 0-7
    fromY = std::stoi(matches[2].str()) - 1; // 1-9 → 0-8
    toX   = matches[3].str()[0] - 'a';
    toY   = std::stoi(matches[4].str()) - 1;

    // 边界检查
    auto isValidPos = [](int x, int y) {
        return x >= 0 && x < 9 && y >= 0 && y < 10;
    };

    return isValidPos(fromX, fromY) && isValidPos(toX, toY);
}

该函数使用正则表达式确保输入符合规范,并完成字符到数组索引的转换。若输入非法,则返回 false ,交由上层逻辑提示重输。

7.1.2 GUI事件捕获与鼠标点击映射

在基于 Qt 的 GUI 实现中,可通过重写 mousePressEvent 捕获点击事件并映射为棋盘坐标:

void ChessBoardWidget::mousePressEvent(QMouseEvent* event) {
    QPoint pos = event->pos();
    int boardX = pos.x() / CELL_SIZE; // CELL_SIZE=60px
    int boardY = pos.y() / CELL_SIZE;

    if (boardX < 0 || boardX >= 9 || boardY < 0 || boardY >= 10) return;

    emit cellClicked(boardX, boardY); // 发信号给主控逻辑
}

此处通过像素坐标除以格子尺寸实现坐标转换,再触发信号通知游戏状态机处理选子或落子逻辑。

7.1.3 异常输入反馈与容错恢复机制

为了提升用户体验,系统应具备容错能力。例如,在连续三次输入错误后提供帮助提示:

class InputValidator {
public:
    bool validateAndLog(const std::string& input) {
        if (parseMoveInput(input, ...)) {
            errorCount = 0;
            return true;
        } else {
            ++errorCount;
            if (errorCount >= 3) {
                showHint(); // 显示合法走法示例
            }
            return false;
        }
    }

private:
    int errorCount = 0;
    void showHint() { 
        std::cout << "提示:请输入类似 \"a0a1\" 的移动指令。\n"; 
    }
};

这种渐进式反馈机制既防止恶意输入导致崩溃,也引导新手逐步掌握操作方式。

7.2 游戏进度保存与文件I/O操作

持久化功能允许玩家中断后继续游戏,支持复盘分析与分享棋局。

7.2.1 使用文本或二进制格式序列化棋局状态

以下是一个简单的文本格式保存方案(FEN-like):

字段 含义
player 当前轮到红方(R)或黑方(B)
board[9][10] 棋子编码矩阵
move_count 回合数

示例 .chess 文件内容:

R
K..A.EB.C.P....P.../............./........./......./........./......./........./C.....C./E.B.P.A.K
56

读取代码片段如下:

FILE* fp = fopen("save.chess", "r");
char turn;
fscanf(fp, "%c\n", &turn);

char boardLine[200];
fgets(boardLine, sizeof(boardLine), fp);

// 解析紧凑字符串为二维数组
int idx = 0;
for (int y = 0; y < 10; ++y) {
    for (int x = 0; x < 9; ++x) {
        char c = boardLine[idx++];
        if (c == '/') c = boardLine[idx++]; // 跳过斜杠
        gameState.board[x][y] = pieceFromChar(c);
    }
}
fclose(fp);

注: pieceFromChar() 'K' 映射为红将, 'k' 为黑将等。

7.2.2 fopen/fwrite等C标准库函数的安全使用

推荐使用 RAII 包装或智能指针管理资源,避免内存泄漏:

struct SafeFile {
    FILE* fp;
    SafeFile(const char* path, const char* mode) : fp(fopen(path, mode)) {}
    ~SafeFile() { if (fp) fclose(fp); }
    operator FILE*() { return fp; }
};

// 使用时自动释放
SafeFile sf("data.bin", "wb");
if (sf) fwrite(&gameState, sizeof(GameState), 1, sf);

此外,建议添加 CRC 校验码防止数据损坏。

7.2.3 加载历史记录与复盘功能实现

可维护一个 std::vector<GameState> 存储每步快照,支持前进/回退:

graph TD
    A[开始游戏] --> B[执行走法]
    B --> C[保存当前状态到历史栈]
    C --> D{是否悔棋?}
    D -->|是| E[弹出栈顶,恢复上一状态]
    D -->|否| B

此结构便于实现“观看对局回放”功能,尤其适用于教学场景。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:本项目是一个基于C/C++语言开发的中国象棋游戏实例,提供完整可下载的源代码,旨在帮助开发者深入理解使用C/C++实现复杂游戏逻辑的全过程。项目涵盖棋盘绘制、棋子规则、用户交互、游戏状态管理等核心功能,涉及数据结构、算法设计、面向对象编程及可能的图形界面开发。通过学习和修改该源码,开发者可掌握游戏开发中的关键编程技术,提升在实际项目中应用C/C++的能力。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

更多推荐