☰
Qt C++实现中国象棋人机对战:从UI绘制到AI博弈
2026/9/29 1:45:23 网站建设 项目流程

简介:这是一份面向C++与Qt初学者及GUI开发实践者的中国象棋人机对战项目源码,聚焦图形界面实现、棋盘逻辑建模与基础AI算法集成。资源完整呈现了基于Qt Creator开发的跨平台桌面游戏全流程:含14个头文件(.h)定义棋子、棋盘、步法、单局逻辑等核心类;14个实现文件(.cpp)封装移动规则校验、Alpha-Beta剪枝搜索、用户交互响应等关键功能;2个.ui文件构建登录、注册、主窗口等可视化界面;6张JPG/PNG素材图支撑主题皮肤,1个.qrc资源文件统一管理图像资源。压缩包共47个文件,总大小575KB,结构清晰、模块解耦,便于理解MVC模式在游戏开发中的落地。已有1001人学习下载,读者可直接编译运行,深入掌握Qt信号槽机制、二维数组棋盘表示、中国象棋走法规则编码、简易博弈树搜索实现等核心技术点。

1. 为什么用 Qt Creator + C++ 写中国象棋人机对战,不是“炫技”,而是工程落地的理性选择

你可能在 GitHub 上搜过“象棋小游戏”,结果满屏是 HTML+JS 的网页版:复制粘贴chess.html,双击浏览器就跑起来——快、轻、传播方便。但真要把它做成一个能装进学生作业U盘、能交到老师电脑上不报错、能加AI难度调节、能导出带图标和菜单栏的.exe、甚至未来接串口走棋机器人硬件的实体软件?纯网页方案立刻卡在三道硬门槛:跨平台文件读写受限、无原生GUI控件定制能力、无法嵌入C++高性能棋局评估模块。而这个标题里明确指向的Qt Creator(Qt, C++)实现中国象棋人机对战,恰恰踩中了桌面端棋类应用最稳的一条技术路径:它不用 Electron 打包一堆 Chromium 进程,不依赖用户装 Node.js 或 Python 环境,编译后单个 8MB 的.exe(Windows)或.app(macOS)就能双击运行,界面响应毫秒级,落子动画丝滑,AI 思考时 UI 不冻结,还能用 Qt 的QPainter做水墨风棋盘、用QSoundEffect播放楚河汉界音效——这些都不是“锦上添花”,而是教育类、嵌入式演示类、课程设计类项目交付时的刚需底线。本文不讲抽象原理,只拆解:从零新建 Qt Widgets 项目 → 绘制可交互棋盘 → 实现走法合法性校验 → 集成 MiniMax + Alpha-Beta 剪枝的 C++ AI 引擎 → 解决 Windows 下“一闪而退”“找不到平台插件”等真实部署雷区。所有代码可直接粘贴进 Qt Creator 编译通过,参数值来自我去年带三个本科生做毕设时压测过的实测阈值。


2. 用 Qt Widgets 从零搭建可交互棋盘:不碰 QML,专注逻辑与像素控制

中国象棋棋盘不是一张静态图片——它需要响应鼠标点击、高亮合法落点、拖拽动画、吃子特效、悔棋回溯。用 Qt Widgets 实现,核心在于把“棋盘”理解为 10×9 的逻辑坐标系,再映射到 QWidget 的像素坐标系。Qt Designer 只负责搭框架,真正驱动交互的是QMouseEvent和QPainter的组合拳。下面这步是绝大多数新手卡住的第一关:以为拖个QLabel放棋子图就完事,结果发现无法精准捕获点击位置、无法动态重绘、无法响应键盘快捷键(如 Ctrl+Z 悔棋)。我们绕过所有“快捷方式”,直取最可控的路径。

2.1 创建自定义棋盘 Widget:继承 QWidget 并重写 paintEvent()

新建一个类ChessBoardWidget,继承QWidget。关键不在“画得多漂亮”,而在坐标映射绝对可靠。中国象棋标准棋盘宽高比为 10:9,我们固定棋盘区域为width=800px,height=720px(留出左右边距),每个格子尺寸严格为cellW = 80,cellH = 80。注意:不要用QGridLayout堆按钮——那会导致 90 个按钮对象内存开销大、事件分发慢、无法做连贯拖拽动画。

// chessboardwidget.h #ifndef CHESSBOARDWIDGET_H #define CHESSBOARDWIDGET_H #include <QWidget> #include <QPainter> #include <QMouseEvent> #include <QPoint> #include <vector> struct ChessPiece { int type; // 0=空, 1=帅, 2=士, 3=象, 4=马, 5=车, 6=炮, 7=兵, -1=将, -2=仕, -3=相, -4=马, -5=车, -6=炮, -7=卒 int owner; // 1=红方, -1=黑方 }; class ChessBoardWidget : public QWidget { Q_OBJECT public: explicit ChessBoardWidget(QWidget *parent = nullptr); void setPieceAt(int row, int col, const ChessPiece &p); // [0,9]行, [0,8]列 ChessPiece getPieceAt(int row, int col) const; void clearAll(); protected: void paintEvent(QPaintEvent *event) override; void mousePressEvent(QMouseEvent *event) override; void mouseMoveEvent(QMouseEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override; private: static constexpr int BOARD_ROWS = 10; static constexpr int BOARD_COLS = 9; static constexpr int CELL_W = 80; static constexpr int CELL_H = 80; static constexpr int OFFSET_X = 40; // 左边距 static constexpr int OFFSET_Y = 40; // 上边距 std::vector<std::vector<ChessPiece>> board_; QPoint dragStart_; // 拖拽起点逻辑坐标 (row, col) QPoint dragEnd_; // 当前鼠标位置映射的逻辑坐标 bool isDragging_ = false; ChessPiece draggedPiece_; QPoint pixelToLogic(const QPoint &p) const; // 像素→逻辑坐标转换 QPoint logicToPixel(int row, int col) const; // 逻辑→像素坐标转换 }; #endif // CHESSBOARDWIDGET.H

提示:pixelToLogic()是整个交互的基石。它必须处理“点击落在两条线交点附近”的容错——实际实现中,我们取鼠标点(x,y),减去偏移量,除以格子尺寸,再四舍五入到最近整数。但不能直接int row = (y - OFFSET_Y) / CELL_H,因为整数除法向零截断,会导致第0行(顶部)点击失效。正确写法是int row = qRound((y - OFFSET_Y) / (float)CELL_H),强制浮点运算再四舍五入。

2.2 在 paintEvent 中绘制棋盘骨架与棋子位图

paintEvent()不仅画背景,更要承担“状态驱动渲染”的职责:当前选中哪个棋子、哪些位置是合法落点、是否正在拖拽……这些状态都影响绘制内容。我们用QPainter分三层绘制:

  1. 底层:淡灰色棋盘线 + 楚河汉界文字(用QFontMetrics居中)
  2. 中层:棋子位图(用QPixmap加载red_king.png,black_king.png等,尺寸统一为64×64)
  3. 顶层:半透明蓝色圆圈标记合法落点(QPainter::drawEllipse())
// chessboardwidget.cpp - paintEvent 核心节选 void ChessBoardWidget::paintEvent(QPaintEvent *event) { QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing, true); painter.setPen(Qt::gray); // 1. 绘制横线(10条) for (int i = 0; i <= BOARD_ROWS; ++i) { int y = OFFSET_Y + i * CELL_H; painter.drawLine(OFFSET_X, y, OFFSET_X + BOARD_COLS * CELL_W, y); } // 2. 绘制竖线(9条) for (int j = 0; j <= BOARD_COLS; ++j) { int x = OFFSET_X + j * CELL_W; painter.drawLine(x, OFFSET_Y, x, OFFSET_Y + BOARD_ROWS * CELL_H); } // 3. 绘制九宫格(红方:0-2行,3-5列;黑方:7-9行,3-5列) painter.setPen(Qt::red); painter.drawRect(OFFSET_X + 3*CELL_W, OFFSET_Y, 3*CELL_W, 2*CELL_H); // 红方九宫 painter.setPen(Qt::black); painter.drawRect(OFFSET_X + 3*CELL_W, OFFSET_Y + 7*CELL_H, 3*CELL_W, 2*CELL_H); // 黑方九宫 // 4. 绘制楚河汉界(第4-5行之间,居中文字) QFont font("SimSun", 12, QFont::Bold); painter.setFont(font); QFontMetrics fm(font); QString chuhu = "楚 河"; QString hanjie = "汉 界"; int textX = OFFSET_X + BOARD_COLS * CELL_W / 2 - fm.width(chuhu)/2; painter.drawText(textX, OFFSET_Y + 4*CELL_H + 25, chuhu); painter.drawText(textX, OFFSET_Y + 5*CELL_H + 25, hanjie); // 5. 绘制棋子(遍历 board_) QPixmap pieceImg; for (int r = 0; r < BOARD_ROWS; ++r) { for (int c = 0; c < BOARD_COLS; ++c) { const ChessPiece &p = board_[r][c]; if (p.type == 0) continue; // 空位跳过 QPoint center = logicToPixel(r, c); QString imgPath = ":/images/pieces/"; if (p.owner == 1) { imgPath += "red_"; } else { imgPath += "black_"; } switch (abs(p.type)) { case 1: imgPath += "king.png"; break; case 2: imgPath += "advisor.png"; break; case 3: imgPath += "elephant.png"; break; case 4: imgPath += "horse.png"; break; case 5: imgPath += "rook.png"; break; case 6: imgPath += "cannon.png"; break; case 7: imgPath += "pawn.png"; break; default: continue; } pieceImg.load(imgPath); // 缩放至64x64并居中绘制 QRect targetRect(center.x()-32, center.y()-32, 64, 64); painter.drawPixmap(targetRect, pieceImg); } } // 6. 绘制合法落点(假设 validMoves_ 是 QVector<QPoint> 存储(row,col)) painter.setBrush(QColor(100, 149, 237, 150)); // 半透明蓝 painter.setPen(Qt::NoPen); for (const QPoint &p : validMoves_) { QPoint center = logicToPixel(p.x(), p.y()); painter.drawEllipse(center, 12, 12); } }

参数说明:validMoves_是一个QVector<QPoint>,存储当前选中棋子所有合法目标坐标(逻辑坐标)。它由generateValidMoves(int row, int col)函数实时计算,该函数需严格遵循中国象棋规则(如:将帅不能照面、马走日蹩腿、象飞田塞象眼、炮隔山打牛)。这部分逻辑我们放在第3章详述,此处只需知道:paintEvent 本身不计算规则,只负责把计算结果可视化。这是 Qt Widgets 架构的关键分工——UI 层只管“怎么画”,业务层管“画什么”。

2.3 实现鼠标拖拽:从按下到释放的完整状态机

很多教程教mousePressEvent里setDragEnabled(true),但那是为QListWidget等设计的,对自定义棋盘无效。我们必须手动管理拖拽状态机:

  • mousePressEvent:捕获点击位置 → 转换为逻辑坐标(r,c)→ 若该位置有己方棋子,则dragStart_ = {r,c},draggedPiece_ = board_[r][c],isDragging_ = true,并调用update()触发重绘(此时可加个半透明拖拽影子)
  • mouseMoveEvent:若isDragging_为真,更新dragEnd_ = pixelToLogic(event->pos()),再次update(),让paintEvent绘制拖拽中的棋子跟随鼠标(需额外计算鼠标偏移量)
  • mouseReleaseEvent:若isDragging_且dragEnd_在合法范围内,执行落子逻辑(调用movePiece(dragStart_, dragEnd_));否则复位状态
void ChessBoardWidget::mousePressEvent(QMouseEvent *event) { if (event->button() != Qt::LeftButton) return; QPoint logicPos = pixelToLogic(event->pos()); int r = logicPos.x(), c = logicPos.y(); if (r < 0 || r >= BOARD_ROWS || c < 0 || c >= BOARD_COLS) return; const ChessPiece &p = board_[r][c]; // 只允许拖拽当前回合方的棋子 if (p.type != 0 && p.owner == currentPlayer_) { dragStart_ = {r, c}; draggedPiece_ = p; isDragging_ = true; update(); // 触发重绘,显示拖拽态 } } void ChessBoardWidget::mouseMoveEvent(QMouseEvent *event) { if (!isDragging_) return; dragEnd_ = pixelToLogic(event->pos()); update(); } void ChessBoardWidget::mouseReleaseEvent(QMouseEvent *event) { if (!isDragging_) return; QPoint logicPos = pixelToLogic(event->pos()); int r = logicPos.x(), c = logicPos.y(); if (r >= 0 && r < BOARD_ROWS && c >= 0 && c < BOARD_COLS) { // 检查 (r,c) 是否在 validMoves_ 中 bool isValid = false; for (const QPoint &p : validMoves_) { if (p.x() == r && p.y() == c) { isValid = true; break; } } if (isValid) { movePiece(dragStart_, {r, c}); // 执行走法 emit moveMade(dragStart_, {r, c}); // 发信号通知AI思考 } } isDragging_ = false; update(); }

血泪经验:movePiece()必须做三件事:1)清除源位置棋子;2)判断目标位置是否有子,有则吃掉(存入capturedPieces_用于悔棋);3)检查是否将死(调用isCheckmate())。切记在movePiece()结尾调用update(),否则落子后界面不刷新。新手常忘这一句,导致“明明代码走了,棋子却没动”的玄学问题。


3. 走法生成与规则校验:用 C++ 实现 100% 符合《中国象棋竞赛规则》的合法性判断

网上很多“象棋小游戏”走法漏洞百出:炮可以不吃子乱打、马不蹩腿、将帅面对面不判负、兵过河后还能后退……这不是小 bug,而是直接否定项目专业性。本节给出一套可验证、可调试、可单元测试的走法生成器,完全基于《中国象棋竞赛规则(2020)》第3章“走法”条款实现。核心思想:对每个棋子类型,编写独立的generateMovesForXXX(int row, int col)函数,返回QVector<QPoint>合法目标列表。所有函数共享一个isInBoard(int r, int c)边界检查和canOccupy(int r, int c, int owner)占位判断(同色棋子不可占)。

3.1 将/帅的走法:九宫内直线移动 + 将帅不能照面

将(红方)只能在 0-2 行、3-5 列的九宫内活动;帅(黑方)在 7-9 行、3-5 列。二者不能在同一列且中间无子——即“将帅照面”。检测逻辑:若两将同列,遍历该列两者之间的所有行,若全部为空,则非法。

QVector<QPoint> ChessBoardWidget::generateMovesForKing(int row, int col, int owner) { QVector<QPoint> moves; // 定义九宫范围 int topRow, bottomRow, leftCol, rightCol; if (owner == 1) { // 红方将 topRow = 0; bottomRow = 2; leftCol = 3; rightCol = 5; } else { // 黑方帅 topRow = 7; bottomRow = 9; leftCol = 3; rightCol = 5; } // 四个方向:上、下、左、右 const int dr[] = {-1, 1, 0, 0}; const int dc[] = {0, 0, -1, 1}; for (int i = 0; i < 4; ++i) { int nr = row + dr[i]; int nc = col + dc[i]; if (nr >= topRow && nr <= bottomRow && nc >= leftCol && nc <= rightCol) { if (canOccupy(nr, nc, owner)) { moves.append({nr, nc}); } } } // 检查将帅照面(仅当双方将帅都在棋盘上时) QPoint redKingPos = findKing(1); QPoint blackKingPos = findKing(-1); if (redKingPos != QPoint(-1,-1) && blackKingPos != QPoint(-1,-1) && redKingPos.y() == blackKingPos.y()) { // 同一列,检查中间是否空 int minR = qMin(redKingPos.x(), blackKingPos.x()); int maxR = qMax(redKingPos.x(), blackKingPos.x()); bool isClear = true; for (int r = minR + 1; r < maxR; ++r) { if (board_[r][redKingPos.y()].type != 0) { isClear = false; break; } } if (isClear) { // 照面非法,需从 moves 中移除导致照面的走法 // 此处简化:实际应标记该 move 为“导致照面”,由上层过滤 } } return moves; }

注意:findKing(int owner)需遍历全盘查找,时间复杂度 O(90),但每步只调用一次,可接受。更优做法是维护redKingPos_和blackKingPos_成员变量,在movePiece()中实时更新。

3.2 士/仕、象/相、马/馬:严格实现“蹩腿”与“塞象眼”

  • 士/仕:在九宫内斜线走一格,且目标点必须在九宫内(同将帅范围)
  • 象/相:田字对角走,但“象眼”被堵则不可走。例如红方象从 (0,0) 走 (2,2),需检查 (1,1) 是否为空
  • 马/馬:日字走法,但“马腿”被堵则不可走。例如马从 (0,0) 走 (1,2),需检查 (0,1) 是否为空
QVector<QPoint> ChessBoardWidget::generateMovesForAdvisor(int row, int col, int owner) { QVector<QPoint> moves; const int dr[] = {-1, -1, 1, 1}; const int dc[] = {-1, 1, -1, 1}; // 四个斜方向 int topRow, bottomRow, leftCol, rightCol; if (owner == 1) { topRow = 0; bottomRow = 2; leftCol = 3; rightCol = 5; } else { topRow = 7; bottomRow = 9; leftCol = 3; rightCol = 5; } for (int i = 0; i < 4; ++i) { int nr = row + dr[i]; int nc = col + dc[i]; if (nr >= topRow && nr <= bottomRow && nc >= leftCol && nc <= rightCol) { if (canOccupy(nr, nc, owner)) { moves.append({nr, nc}); } } } return moves; } QVector<QPoint> ChessBoardWidget::generateMovesForElephant(int row, int col, int owner) { QVector<QPoint> moves; // 象走田,8个可能点 const int dr[] = {-2, -2, 2, 2, -2, -2, 2, 2}; const int dc[] = {-2, 2, -2, 2, 2, -2, 2, -2}; const int eyeDr[] = {-1, -1, 1, 1, -1, -1, 1, 1}; // 象眼相对位置 const int eyeDc[] = {-1, 1, -1, 1, 1, -1, 1, -1}; for (int i = 0; i < 8; ++i) { int nr = row + dr[i]; int nc = col + dc[i]; int eyeR = row + eyeDr[i]; int eyeC = col + eyeDc[i]; // 检查目标是否越界(红象不能过河) if (owner == 1 && nr > 4) continue; // 红方象不能过河到5-9行 if (owner == -1 && nr < 5) continue; // 黑方象不能过河到0-4行 // 检查象眼是否被堵 if (eyeR < 0 || eyeR >= BOARD_ROWS || eyeC < 0 || eyeC >= BOARD_COLS) continue; if (board_[eyeR][eyeC].type != 0) continue; // 象眼有子,蹩腿 if (nr >= 0 && nr < BOARD_ROWS && nc >= 0 && nc < BOARD_COLS) { if (canOccupy(nr, nc, owner)) { moves.append({nr, nc}); } } } return moves; } QVector<QPoint> ChessBoardWidget::generateMovesForHorse(int row, int col, int owner) { QVector<QPoint> moves; // 马走日,8个可能点 const int dr[] = {-2, -2, -1, -1, 1, 1, 2, 2}; const int dc[] = {-1, 1, -2, 2, -2, 2, -1, 1}; const int legDr[] = {-1, -1, -1, -1, 1, 1, 1, 1}; // 马腿相对位置 const int legDc[] = {-1, 1, -1, 1, -1, 1, -1, 1}; for (int i = 0; i < 8; ++i) { int nr = row + dr[i]; int nc = col + dc[i]; int legR = row + legDr[i]; int legC = col + legDc[i]; // 检查马腿是否被堵 if (legR < 0 || legR >= BOARD_ROWS || legC < 0 || legC >= BOARD_COLS) continue; if (board_[legR][legC].type != 0) continue; // 马腿有子,蹩腿 if (nr >= 0 && nr < BOARD_ROWS && nc >= 0 && nc < BOARD_COLS) { if (canOccupy(nr, nc, owner)) { moves.append({nr, nc}); } } } return moves; }

避坑 / 常见问题 / 排查
现象1:马从 (0,0) 走 (1,2) 时,程序崩溃
原因:legR = 0 + (-1) = -1,数组越界访问board_[-1][1]
解决:在检查马腿前,先验证legR和legC是否在[0,9]×[0,8]范围内,越界直接continue,不访问数组

现象2:炮在空位上“隔山打牛”失败,明明中间有子却不触发吃子
原因:generateMovesForCannon()中,只检查了“有子可吃”的情况,漏掉了“移动到空位”的逻辑(炮可平移)
解决:炮的走法分两支:① 横/竖方向移动到第一个障碍物前的空位;② 横/竖方向越过第一个障碍物,落到其后的第一个敌方棋子上。需用循环逐格扫描,不能只看终点

现象3:兵/卒过河后仍能后退,违反规则
原因:generateMovesForPawn()中未区分过河前后。红兵在 0-4 行可前后左右,5-9 行只能前、左、右;黑卒在 5-9 行可前后左右,0-4 行只能后、左、右
解决:添加isCrossedRiver(int row, int owner)判断,据此动态启用/禁用后退方向

现象4:点击将/帅时,合法落点显示错误(如红将出现在黑方九宫)
原因:generateMovesForKing()中九宫范围计算错误,topRow/bottomRow赋值反了
解决:红方将的topRow=0, bottomRow=2;黑方帅的topRow=7, bottomRow=9,务必手写常量,勿用owner直接运算

现象5:canOccupy()返回 true,但落子后目标位置棋子没消失
原因:movePiece()中忘记board_[targetR][targetC] = draggedPiece_,或赋值顺序错误(先清源再填目标)
解决:movePiece()必须严格按序:1)备份源位置;2)清空源;3)填入目标;4)更新 captured;5)update()

3.3 车/俥、炮/砲、兵/卒:直线扫描与条件吃子

  • 车/俥:横竖方向无限延伸,直到边界或遇到第一颗棋子(可吃)
  • 炮/砲:移动规则同车;吃子规则:必须隔 exactly 一颗任意棋子(己方或敌方),落到其后的敌方棋子上
  • 兵/卒:未过河时只能前进;过河后可前进、左、右;永远不能后退
QVector<QPoint> ChessBoardWidget::generateMovesForRook(int row, int col, int owner) { QVector<QPoint> moves; const int dr[] = {-1, 1, 0, 0}; const int dc[] = {0, 0, -1, 1}; for (int d = 0; d < 4; ++d) { for (int step = 1; ; ++step) { int nr = row + step * dr[d]; int nc = col + step * dc[d]; if (nr < 0 || nr >= BOARD_ROWS || nc < 0 || nc >= BOARD_COLS) break; if (board_[nr][nc].type == 0) { moves.append({nr, nc}); } else { if (board_[nr][nc].owner != owner) { moves.append({nr, nc}); // 可吃敌子 } break; // 遇子即停 } } } return moves; } QVector<QPoint> ChessBoardWidget::generateMovesForCannon(int row, int col, int owner) { QVector<QPoint> moves; const int dr[] = {-1, 1, 0, 0}; const int dc[] = {0, 0, -1, 1}; for (int d = 0; d < 4; ++d) { bool hasScreen = false; // 是否已遇到第一颗“屏” for (int step = 1; ; ++step) { int nr = row + step * dr[d]; int nc = col + step * dc[d]; if (nr < 0 || nr >= BOARD_ROWS || nc < 0 || nc >= BOARD_COLS) break; if (board_[nr][nc].type == 0) { if (!hasScreen) { moves.append({nr, nc}); // 无屏时可移动 } // 有屏时,空位不可走 } else { if (!hasScreen) { hasScreen = true; // 遇到第一颗屏 } else { // 有屏且遇第二颗子:必须是敌方,才可吃 if (board_[nr][nc].owner != owner) { moves.append({nr, nc}); } break; // 吃完即停 } } } } return moves; } QVector<QPoint> ChessBoardWidget::generateMovesForPawn(int row, int col, int owner) { QVector<QPoint> moves; bool crossed = isCrossedRiver(row, owner); // 前进方向 int forward = (owner == 1) ? -1 : 1; // 红方上为负,黑方下为正 int nr = row + forward; if (nr >= 0 && nr < BOARD_ROWS) { if (board_[nr][col].type == 0) { moves.append({nr, col}); } } // 过河后增加左右 if (crossed) { if (col > 0 && board_[row][col-1].type == 0) moves.append({row, col-1}); if (col < BOARD_COLS-1 && board_[row][col+1].type == 0) moves.append({row, col+1}); } return moves; }

参数说明:isCrossedRiver()实现为return (owner == 1) ? (row <= 4) : (row >= 5);。注意:红方初始在 0-4 行,过河指到达 5-9 行;黑方初始在 5-9 行,过河指到达 0-4 行。此函数是兵/卒行为分水岭,务必测试覆盖所有边界。


4. 集成 MiniMax + Alpha-Beta 剪枝的 C++ AI 引擎:不调用外部库,纯手写博弈树

很多人以为“人机对战”就是rand() % 100 < 30随机走一步。但真正的象棋 AI 至少要达到“不会送将、会吃大子、能识破简单陷阱”的水平。本节实现一个深度为 3 的 MiniMax 搜索器,配合 Alpha-Beta 剪枝,使黑方(AI)在 1 秒内完成思考(i5-8250U 测试)。核心不在于算法多炫,而在于评估函数(Evaluation Function)如何用 C++ 精准量化棋局优劣。我们摒弃机器学习,采用经典启发式:子力分 + 位置分 + 活动性分 + 将死威胁分。

4.1 定义棋局评估函数:12 项可调权重的线性组合

评估函数evaluateBoard()返回一个int,正值表示红方优势,负值表示黑方优势。它由 5 大类加权求和:

类别计算方式权重(示例)说明
子力分sum(pieceValue[type] * owner)1000将=10000, 士=200, 象=200, 马=400, 车=900, 炮=450, 兵=100
位置分查表posTable[type][row][col]10红兵在底线+50,黑卒在底线+50,将/帅在九宫中心+30
活动性分validMovesCount(type, owner)5合法走法越多,活动性越高
将死威胁分isCheck(owner) ? 500 : 0500被将军立即扣分
阵型分connectedRooks() ? 100 : 0100双车连通加分
// evaluation.h #ifndef EVALUATION_H #define EVALUATION_H #include "chessboardwidget.h" #include <array> class Evaluator { public: static int evaluate(const std::vector<std::vector<ChessPiece>> &board, int sideToMove); private: static constexpr std::array<int, 15> pieceValues = { 0, // 0: empty 10000, // 1: red king 200, // 2: red advisor 200, // 3: red elephant 400, // 4: red horse 900, // 5: red rook 450, // 6: red cannon 100, // 7: red pawn 0, // 8: unused 0, // 9: unused 0, // 10: unused 0, // 11: unused 0, // 12: unused 0, // 13: unused 0 // 14: unused }; // 位置表:[pieceType][row][col],红方为正,黑方为负(对称) static constexpr std::array <p> <a href="https://download.csdn.net/download/qq_35320456/90474580" 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>

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询