C++与MFC实现中国象棋:从桌面开发到AI对战引擎的完整实践

📅 2026/8/11 12:47:20
C++与MFC实现中国象棋:从桌面开发到AI对战引擎的完整实践
1. 项目概述与核心价值最近在整理硬盘里的老项目翻出来一个大学时期写的中国象棋游戏用C和MFC框架搭的还带一个简单的AI对战功能。当时为了写这个没少熬夜查资料、调试棋盘逻辑。现在回头看虽然界面有点“复古”但整个项目的架构和实现思路对于想深入理解Windows桌面程序开发、游戏逻辑设计甚至是入门级AI算法的朋友来说依然是个不错的练手素材。这个项目不只是一个“能下棋的程序”它更像一个微型的综合实验室把面向对象设计、消息驱动机制、图形绘制和搜索算法这些知识点都串了起来。如果你是一名C初学者或者对MFC这套经典的Windows GUI框架感兴趣想从“黑框控制台”跨越到“带界面的应用程序”那么这个象棋项目会是一个很好的台阶。它没有复杂的网络通信和数据库核心就是棋盘状态的管理、鼠标事件的响应以及AI的走子计算。通过拆解它你能清晰地看到一个桌面应用是如何从数据到视图再到用户交互的完整循环。而对于已经有一定基础想挑战“游戏AI”这个领域的开发者里面实现的简单极大极小值搜索算法则是理解更复杂AI比如Alpha-Beta剪枝、蒙特卡洛树搜索的绝佳起点。项目的目标很明确构建一个功能完整、逻辑正确、且具备基本人机对战能力的中国象棋程序。这意味着你需要处理好几个核心模块第一是游戏规则引擎也就是判断“马走日、象走田”、将军、困毙等所有象棋规则的正确代码化第二是图形用户界面用MFC来绘制棋盘、棋子并响应玩家的鼠标操作第三是AI引擎让电脑能像一个真正的对手一样思考并走出一步棋。把这三点打通一个五脏俱全的象棋游戏就诞生了。2. 技术栈选型与项目架构解析2.1 为什么是C和MFC首先得聊聊技术选型。用C来实现中国象棋在今天看来可能不是最“时髦”的选择但却是最“扎实”和“教育意义”的。C能让你对内存管理、对象生命周期、性能优化有最直接的感知。象棋棋盘和棋子天然适合用类和对象来建模。比如一个CChessPiece类可以抽象出棋子的颜色、类型车、马、炮等、位置坐标以及最重要的——根据规则计算所有合法走法的函数。这种面向对象的设计让代码逻辑非常清晰。而选择MFCMicrosoft Foundation Classes作为GUI框架在当时的Windows桌面开发环境下是非常自然的选择。MFC封装了Win32 API用起来比直接调用API方便不少但又不像后来的.NET框架那样高度封装保留了足够的底层控制力。对于这个项目MFC提供了几个关键能力一是CDC设备上下文类让我们可以方便地在窗口上绘制线条棋盘和位图棋子二是消息映射机制能优雅地处理鼠标点击、窗口重绘等事件三是对话框和控件可以用来做游戏设置、显示提示信息等。当然现在新建项目可能更多会考虑Qt、wxWidgets甚至Dear ImGui。但学习MFC的价值在于它能帮你理解Windows桌面程序“消息循环”这一核心工作机制。当你用OnLButtonDown函数处理鼠标左键点击用OnPaint函数处理窗口重绘时你实际上是在与Windows操作系统进行对话。这种理解对于后续学习任何GUI框架都大有裨益。2.2 项目整体架构设计这个象棋程序的架构可以清晰地分为三层这也是很多桌面游戏程序的通用模式。模型层Model这是游戏的核心大脑完全独立于界面。它主要包含一个CGameEngine或CBoard类负责维护当前的棋盘状态。这个状态通常用一个9x10的二维数组对应象棋棋盘9条竖线、10条横线的交叉点来表示每个元素存储的是何种棋子或者为空。模型层还要提供一系列关键接口MakeMove(From, To)尝试执行一步从位置From到To的移动并返回是否合法。GenerateAllMoves(side)为某一方红方或黑方生成当前所有合法走法。IsCheck(side)判断某一方是否被将军。IsCheckmate(side)判断某一方是否被将死。 模型层的设计原则是“纯净”它不应该知道任何关于图形、鼠标的事情只关心棋盘逻辑。这样便于单独测试也为将来替换GUI或AI模块提供了可能。视图层View这一层由MFC的窗口类如CMyChessView负责。它的任务是根据模型层的当前状态把棋盘和棋子画出来。通常我们在OnPaint函数里做这件事先画十条横线、九条竖线再画楚河汉界然后遍历棋盘数组根据棋子类型和坐标在相应的交叉点位置绘制棋子的图片或文字。视图层是“被动”的它不修改游戏状态只负责展示。控制层Controller这是连接用户、界面和模型的桥梁。在MFC中它通常融合在视图类里通过消息处理函数来实现。例如OnLButtonDown用户点击鼠标控制器需要将屏幕坐标转换为棋盘坐标第几行第几列。如果之前没有选中棋子则判断点击位置是否有己方棋子有则高亮选中它。如果之前已选中一个棋子则再次点击时控制器会调用模型层的MakeMove函数验证这步棋是否合法。如果合法则更新模型状态并触发视图重绘Invalidate同时如果是人机对战模式在玩家走完后要启动AI思考流程。AI模块可以看作是控制器的一个特殊部分或者一个独立的“玩家”对象。当轮到AI走棋时控制器会调用AI的Think或FindBestMove方法。AI内部会访问模型层进行局面分析和搜索最终返回一个最优的走法然后控制器再通过MakeMove执行这个走法。这种MVC或MVC变体的架构使得代码职责分明维护和扩展起来会轻松很多。比如你想把棋子图片从位图换成更精美的PNG只需要修改视图层的绘制代码你想增强AI的棋力只需要替换AI模块模型和控制器基本不用动。3. 核心模块实现细节与难点攻克3.1 游戏规则引擎的实现这是整个项目最需要严谨对待的部分规则上的一点小漏洞都会导致游戏无法进行或者出现“耍赖”的走法。我们首先要定义棋盘的数据结构。// 棋子类型枚举 enum PieceType { PIECE_NONE, ROOK, KNIGHT, BISHOP, ADVISOR, KING, CANNON, PAWN }; // 棋子颜色枚举 enum PieceColor { COLOR_NONE, RED, BLACK }; // 棋盘上的一个点 struct ChessPoint { int x; // 0-8 对应棋盘从左到右的9条竖线 int y; // 0-9 对应棋盘从下到上的10条横线红方在下黑方在上 }; // 棋子类 class CChessPiece { public: PieceType type; PieceColor color; ChessPoint position; bool isAlive; // 关键函数根据棋子类型和当前棋盘状态计算所有可能的合法目标位置 std::vectorChessPoint GetPossibleMoves(const CBoard board) const; };走法生成每个棋子的走法规则需要单独实现。以“马”为例它的走法是“马走日”但同时要处理“蹩马腿”的情况。std::vectorChessPoint CChessPiece::GetPossibleMoves(const CBoard board) const { std::vectorChessPoint moves; if (type ! KNIGHT) return moves; // 仅以马为例 if (!isAlive) return moves; // 马可以走的8个相对方向 int dx[8] { 1, 2, 2, 1, -1, -2, -2, -1 }; int dy[8] { 2, 1, -1, -2, -2, -1, 1, 2 }; // 对应的马腿位置马走日如果日的“竖”被挡则不能走 int legDx[8] { 0, 1, 1, 0, 0, -1, -1, 0 }; int legDy[8] { 1, 0, 0, -1, -1, 0, 0, 1 }; for (int i 0; i 8; i) { ChessPoint legPos { position.x legDx[i], position.y legDy[i] }; // 检查马腿位置是否有任何棋子无论敌我 if (board.GetPieceAt(legPos).type ! PIECE_NONE) { continue; // 马腿被蹩这个方向不能走 } ChessPoint targetPos { position.x dx[i], position.y dy[i] }; // 检查目标位置是否在棋盘内 if (!board.IsPositionValid(targetPos)) continue; // 检查目标位置是否有己方棋子不能吃己方 PieceColor targetColor board.GetPieceAt(targetPos).color; if (targetColor color) continue; // 如果通过所有检查则这是一个合法走法 moves.push_back(targetPos); } return moves; }其他棋子的规则类似但各有特点“车”要直线行走直到遇到障碍“炮”吃子时需要中间隔一个棋子“将/帅”不能出九宫格且不能面对面等。把这些规则都准确无误地编码实现是游戏逻辑正确的基石。将军与将死的判断这是规则引擎的另一个难点。判断是否将军需要模拟对方走完一步后的局面检查己方的“王”是否处于对方任何棋子的攻击范围内。而判断将死则需要为被将军的一方生成所有可能的走法然后逐一尝试看是否存在一步棋能解除将军状态。如果所有走法尝试后仍然被将军那就是将死了。注意在实现规则时要特别注意棋子的坐标体系。通常我们以棋盘左下角为原点(0,0)红方在下y值小黑方在上y值大。但在绘制和鼠标交互时屏幕坐标的原点在左上角需要进行转换。建议在项目初期就统一好坐标系并写好转换函数避免后续混乱。3.2 MFC图形界面与交互实现图形界面部分主要工作是绘制和响应事件。我们通常在一个从CView或CDialog派生的类中完成。1. 资源准备首先需要棋盘的背景图和棋子图片。可以准备两套棋子位图红方和黑方每套包含7种棋子。将这些位图作为资源导入到MFC工程中.rc文件。2. 绘制棋盘在视图类的OnDraw或OnPaint函数中进行。void CChessView::OnDraw(CDC* pDC) { // 1. 绘制棋盘网格和背景 CPen gridPen(PS_SOLID, 2, RGB(0, 0, 0)); CPen* pOldPen pDC-SelectObject(gridPen); // 画横线 for (int row 0; row 9; row) { pDC-MoveTo(m_leftMargin, m_topMargin row * m_cellSize); pDC-LineTo(m_leftMargin 8 * m_cellSize, m_topMargin row * m_cellSize); } // 画竖线注意中间缺一条 // ... 省略具体代码 pDC-SelectObject(pOldPen); // 2. 绘制棋子 CBoard* pBoard GetDocument()-GetBoard(); // 假设通过文档类获取模型 for (int y 0; y 10; y) { for (int x 0; x 9; x) { CChessPiece piece pBoard-GetPieceAt(x, y); if (piece.type PIECE_NONE) continue; // 计算棋子绘制中心点 CPoint drawPos BoardToClient(ChessPoint{x, y}); // 根据棋子类型和颜色选择对应的位图资源ID UINT nResID GetPieceBitmapID(piece.type, piece.color); CBitmap bmp; bmp.LoadBitmap(nResID); // 创建内存DC进行透明绘制避免白边 CDC memDC; memDC.CreateCompatibleDC(pDC); CBitmap* pOldBmp memDC.SelectObject(bmp); BITMAP bm; bmp.GetBitmap(bm); // 使用透明背景方式绘制 pDC-TransparentBlt(drawPos.x - bm.bmWidth/2, drawPos.y - bm.bmHeight/2, bm.bmWidth, bm.bmHeight, memDC, 0, 0, bm.bmWidth, bm.bmHeight, RGB(255, 255, 255)); memDC.SelectObject(pOldBmp); } } // 3. 绘制选中高亮如果有棋子被选中 if (m_selectedPos.x 0) { CPoint hlPos BoardToClient(m_selectedPos); CBrush hlBrush(RGB(255, 255, 0)); // 黄色高亮 CBrush* pOldBrush pDC-SelectObject(hlBrush); pDC-SelectStockObject(NULL_PEN); pDC-Ellipse(hlPos.x - m_cellSize/3, hlPos.y - m_cellSize/3, hlPos.x m_cellSize/3, hlPos.y m_cellSize/3); pDC-SelectObject(pOldBrush); } }3. 鼠标交互这是控制层的核心。void CChessView::OnLButtonDown(UINT nFlags, CPoint point) { // 1. 将客户端坐标转换为棋盘坐标 ChessPoint boardPos ClientToBoard(point); if (!IsPositionValid(boardPos)) return; CBoard* pBoard GetDocument()-GetBoard(); PieceColor currentSide pBoard-GetCurrentSide(); // 当前该谁走 // 2. 如果之前没有选中棋子 if (m_selectedPos.x 0) { CChessPiece clickedPiece pBoard-GetPieceAt(boardPos); // 如果点击位置有棋子并且是该棋子的颜色走 if (clickedPiece.type ! PIECE_NONE clickedPiece.color currentSide) { m_selectedPos boardPos; // 选中它 Invalidate(); // 触发重绘显示高亮 } } // 3. 如果之前已经选中了一个棋子 else { // 尝试走子从 m_selectedPos 走到 boardPos if (pBoard-MakeMove(m_selectedPos, boardPos)) { // 走子成功 m_selectedPos ChessPoint{-1, -1}; // 清空选中 Invalidate(); // 重绘新棋盘 // 如果是人机对战模式且游戏未结束则启动AI思考 if (m_gameMode MODE_AI !pBoard-IsGameOver()) { // 可以启动一个后台线程或者设置定时器来异步执行AI思考避免界面卡死 SetTimer(AI_THINK_TIMER, 100, NULL); // 给AI 100毫秒“思考”时间 } } else { // 走子非法比如不符合规则 // 可以播放一个错误音效或者简单地取消选中 m_selectedPos ChessPoint{-1, -1}; Invalidate(); } } CView::OnLButtonDown(nFlags, point); }实操心得在MFC中频繁的Invalidate()和重绘可能导致界面闪烁。一个常见的优化方法是使用双缓冲绘图。即先在内存中创建一个兼容的位图CDC和CBitmap把所有东西画到这个内存DC上然后一次性将整个位图BitBlt到屏幕DC。这能极大提升绘制流畅度。另外棋子图片建议使用带透明通道的PNG格式虽然MFC原生支持不好但可以通过GDI库Gdiplus.h来加载和绘制效果会好很多。3.3 简单AI对战引擎的实现让电脑下棋核心是教它如何评估一个局面并搜索未来几步可能发生的情况选择对自己最有利的一步。这里实现一个最基础的极大极小值搜索算法。1. 局面评估函数这是AI的“价值观”告诉它当前棋盘谁占优。一个最简单的评估函数就是计算棋子价值总和。int CSimpleAI::EvaluateBoard(const CBoard board, PieceColor aiSide) { int score 0; const int pieceValue[7] {0, 500, 300, 300, 200, 100000, 300, 100}; // 空车马象士将炮兵 的估值 for (int y 0; y 10; y) { for (int x 0; x 9; x) { CChessPiece p board.GetPieceAt(x, y); if (p.type PIECE_NONE) continue; int value pieceValue[p.type]; // 如果是AI的棋子加分如果是玩家的棋子减分 score (p.color aiSide) ? value : -value; } } // 可以加入一些位置分比如过河的兵价值更高马在河口的控制力更强等 // score CalculatePositionalScore(board, aiSide); return score; }这个评估非常粗糙但足以让AI有一个“车比马贵马比炮贵”的基本概念。更高级的评估会考虑棋子位置、灵活性、对关键点的控制等。2. 极大极小值搜索AI假设双方在接下来的N步内都会走出对自己最有利的棋。极大层AI走棋从所有可能走法中选择一个能让评估分数最大化的走法因为分数是从AI视角算的分数高对AI有利。极小层玩家走棋假设玩家会从所有可能走法中选择一个能让评估分数最小化的走法对AI最不利。 算法通过递归模拟未来几步的走棋过程。// 极大极小值搜索depth是剩余搜索深度alpha和beta用于后续可选的Alpha-Beta剪枝优化 int CSimpleAI::Minimax(CBoard board, int depth, PieceColor currentSide, int alpha, int beta) { // 递归终止条件达到搜索深度或者游戏已结束 if (depth 0 || board.IsGameOver()) { return EvaluateBoard(board, m_aiColor); } // 生成当前局面下当前行棋方的所有合法走法 std::vectorCMove moves board.GenerateAllMoves(currentSide); // 对走法进行排序好的走法放前面能极大提升后续剪枝效率 OrderMoves(moves, board); if (currentSide m_aiColor) { // AI走棋极大层 int maxEval INT_MIN; for (const CMove move : moves) { // 尝试走这一步 board.MakeMove(move.from, move.to); int eval Minimax(board, depth - 1, GetOppositeColor(currentSide), alpha, beta); board.UndoMove(); // 关键撤销这一步恢复棋盘状态 maxEval std::max(maxEval, eval); alpha std::max(alpha, eval); if (beta alpha) { break; // Beta剪枝 } } return maxEval; } else { // 玩家走棋极小层 int minEval INT_MAX; for (const CMove move : moves) { board.MakeMove(move.from, move.to); int eval Minimax(board, depth - 1, GetOppositeColor(currentSide), alpha, beta); board.UndoMove(); minEval std::min(minEval, eval); beta std::min(beta, eval); if (beta alpha) { break; // Alpha剪枝 } } return minEval; } }3. 走法选择AI的入口函数就是调用这个搜索然后从第一层的走法中选择评估值最高的那个。CMove CSimpleAI::FindBestMove(CBoard board) { std::vectorCMove moves board.GenerateAllMoves(m_aiColor); if (moves.empty()) return CMove(); // 无棋可走 CMove bestMove; int bestValue INT_MIN; int searchDepth 3; // 搜索深度例如3层AI-玩家-AI for (const CMove move : moves) { board.MakeMove(move.from, move.to); // 搜索时下一层是玩家走棋极小层 int moveValue Minimax(board, searchDepth - 1, GetOppositeColor(m_aiColor), INT_MIN, INT_MAX); board.UndoMove(); if (moveValue bestValue) { bestValue moveValue; bestMove move; } } return bestMove; }搜索深度设为3意味着AI会思考“如果我走A对方可能会走B然后我再走C最终局面我大概能得多少分”。深度越大AI越强但计算时间呈指数级增长。深度为3或4对于反应迅速的简单AI已经足够。注意事项极大极小值搜索的核心是棋盘状态的备份与恢复。在递归尝试每一步棋时必须MakeMove后在递归返回时立即UndoMove将棋盘恢复到尝试前的状态。这就要求你的CBoard类必须支持UndoMove操作通常用一个栈来记录每一步的移动信息什么棋子从哪走到哪是否吃子等。这是实现搜索算法的关键也是最容易出错的地方。4. 工程化实践与高级功能探讨4.1 项目组织与代码结构一个清晰的代码结构能让项目更容易理解和维护。建议按如下方式组织你的Visual Studio解决方案MyChineseChess/ ├── ChessGame.sln ├── ChessGame/ │ ├── stdafx.h, stdafx.cpp // 预编译头 │ ├── targetver.h │ ├── ChessGame.h, ChessGame.cpp // 主应用类 │ ├── MainFrm.h, MainFrm.cpp // 主框架窗口 │ ├── ChessGameDoc.h, ChessGameDoc.cpp // 文档类作为数据容器 │ ├── ChessGameView.h, ChessGameView.cpp // 视图类负责绘制和交互 │ ├── BoardEngine/ // 核心规则引擎 │ │ ├── Board.h, Board.cpp // 棋盘状态管理 │ │ ├── Piece.h, Piece.cpp // 棋子类 │ │ ├── Move.h, Move.cpp // 走法表示 │ │ └── RuleChecker.h, RuleChecker.cpp // 规则校验器 │ ├── AI/ // AI引擎 │ │ ├── AIBase.h // AI基类接口 │ │ ├── SimpleMiniMaxAI.h, SimpleMiniMaxAI.cpp // 极大极小AI实现 │ │ └── Evaluator.h, Evaluator.cpp // 局面评估器 │ └── Resources/ // 资源文件 │ ├── ChessGame.rc │ ├── res/ │ │ ├── board.bmp │ │ ├── piece_red_rook.bmp │ │ └── ... │ └── ChessGame.ico └── Readme.txt将游戏逻辑BoardEngine和AI逻辑AI独立成单独的目录或项目与MFC的界面代码View,Doc解耦。这样设计的好处是你可以很方便地替换AI引擎比如换成一个更强大的基于神经网络AI或者甚至将核心逻辑移植到其他平台比如用Qt重写界面而无需重写游戏规则。4.2 性能优化与高级AI思路基础的极大极小值搜索在深度加深后计算量会急剧膨胀。这里有几个实用的优化方向1. Alpha-Beta剪枝上文代码中已经包含了剪枝的框架alpha和beta参数。它的原理是在搜索过程中如果发现某个分支的走势已经明显差于之前发现的另一个分支那么就没必要继续深入搜索这个“坏分支”了。正确实现Alpha-Beta剪枝可以在不改变搜索结果的前提下大幅减少需要评估的节点数有时能带来数十倍的性能提升。实现的关键在于对走法进行排序优先搜索那些看起来最好的走法如吃子、将军这样能更早地触发剪枝条件。2. 走法排序在递归搜索每一层的走法列表时不要随机顺序尝试。一个好的排序策略能极大提升Alpha-Beta剪枝的效率。一个简单的排序可以是优先尝试吃子的走法特别是吃价值高的棋子。其次尝试将军的走法。再次尝试移动到中心或有利位置的走法。最后尝试其他走法。3. 迭代加深与时间控制不要固定搜索深度。可以采用迭代加深先搜索1层得到最佳走法和估值如果没有超时再搜索2层更新结果接着搜索3层……这样即使时间突然不够我们也能有一个已经计算好的、较浅深度的结果作为备选而不是一无所获。同时在每次递归调用前检查用时如果超过预定时间比如1秒则立即停止搜索并返回当前找到的最佳走法。4. 局面评估函数优化这是提升AI棋力的核心。除了子力价值可以加入更多位置价值。例如为棋盘上每个位置定义一个分数表兵种不同表也不同。马在河口棋盘中央区域的控制力更强分数就高车在巡河位置己方河界很有威胁过河的兵尤其是靠近九宫的兵价值倍增。还可以加入一些局势特征的评估比如棋子的灵活性有多少个可走的位置、对关键点将门、肋道的控制、双方士象的完整程度等。一个复杂的评估函数可能需要上百个权重参数这些参数可以通过自我对弈或棋谱学习来调整。5. 开局库与残局库对于象棋AI还有一个“作弊”般的优化——使用开局库和残局库。开局库存储了经过千百年验证的经典开局套路如中炮对屏风马AI在开局阶段可以直接从库中选取走法又快又准。残局库则存储了子力很少时比如单车对士象全的必胜、必和定式AI在残局阶段可以查询库来得到绝对正确的走法。对于个人项目可以手动录入一些简单的开局和杀法。4.3 功能扩展与趣味性增强完成基础版本后你可以考虑为你的象棋程序添加更多功能让它更像一个完整的软件产品1. 游戏状态管理悔棋功能在CBoard类中维护一个走法历史栈。悔棋时从栈顶弹出一步并执行UndoMove。注意在人机对战模式下悔棋需要连续撤销玩家和AI的两步。保存与加载将棋盘状态二维数组、当前行棋方、步数等信息序列化到文件。可以设计一个简单的文本格式比如用字符表示棋子R红车r黑车便于调试和分享棋局。棋谱记录用标准的象棋棋谱格式如“炮二平五”、“马8进7”记录每一步棋并支持导出为文件或复盘播放。2. 界面与用户体验优化音效为走棋、吃子、将军、胜利等事件添加音效提升沉浸感。动画效果让棋子的移动不是瞬间跳过去而是有一个平滑的移动动画。这可以通过在OnTimer函数中不断更新棋子的绘制位置来实现。网络对战这是一个更大的挑战。你需要设计一个简单的通信协议将走法坐标序列化通过Socket在两端传递。可以先用本地回环地址测试再尝试局域网对战。3. AI难度分级通过调整搜索深度和评估函数的复杂度可以轻松实现多个难度级别。初级搜索深度2使用简单的子力评估。中级搜索深度3-4加入基本的位置评估。高级搜索深度4-5使用更复杂的评估函数并开启开局库。 这样可以让不同水平的玩家都能找到乐趣。5. 常见问题排查与调试技巧在开发过程中你肯定会遇到各种奇怪的问题。下面是一些典型问题的排查思路1. 棋子走法规则错误现象马可以走“田”字炮可以随便飞。排查单独测试每个棋子的GetPossibleMoves函数。写一个简单的测试程序在控制台打印出棋子在特定位置的所有合法走法与象棋规则手册对照。特别注意“蹩马腿”、“炮架”、“将帅不能照面”这些特殊规则的边界条件。2. AI走棋太慢或程序卡死现象选择AI对战后程序界面卡住很久才有反应甚至无响应。排查首先检查搜索深度是否设置过高。从深度2开始测试。在Minimax函数入口和出口添加日志输出当前深度和评估的节点数观察递归规模。确保你的GenerateAllMoves函数是高效的。如果每生成一步走法都进行大量的动态内存分配或字符串操作会极大拖慢速度。尽量使用预分配的内存池或静态数组。最重要的一点不要在UI主线程中进行深度搜索这会导致界面消息无法处理看起来就是卡死。正确的做法是开启一个工作线程专门进行AI计算或者使用定时器SetTimer将AI思考分片进行。在工作线程中计算完成后通过发送自定义消息PostMessage通知主线程更新界面。3. 图形界面闪烁或绘制残影现象移动窗口或走棋时棋盘闪烁或者棋子留下拖影。解决启用双缓冲。在MFC中可以在视图类中重写OnEraseBkgnd函数并直接返回TRUE阻止Windows擦除背景然后在OnPaint中全部使用双缓冲绘制。确保每次OnPaint都绘制完整的棋盘不要依赖之前的图像。棋子图片的背景色最好与棋盘背景色一致或者使用支持透明色的绘制方法如TransparentBlt或GDI。4. 鼠标点击位置不准确现象点击棋子没反应或者点在一个交叉点却选中了旁边的棋子。排查仔细检查ClientToBoard坐标转换函数。确认你的棋盘绘制起始位置m_leftMargin,m_topMargin和格子大小m_cellSize计算正确。可以在OnPaint中绘制出每个交叉点的坐标数字并在OnLButtonDown时打印转换后的棋盘坐标进行比对调试。5. 悔棋后状态异常现象执行悔棋操作后棋盘显示错误或者AI接着走时出错。排查确保MakeMove和UndoMove是严格对称的。每走一步除了改变棋盘数组还要记录足够的信息被吃的棋子、是否将军过等以便完全恢复。悔棋后当前行棋方要正确切换回去。如果AI正在思考应该中断其思考过程并清除相关的状态。6. 内存泄漏现象长时间运行游戏后程序占用内存越来越大。排查在Visual Studio中使用“诊断工具”窗口监测内存使用。重点检查每次OnPaint中创建的CPen,CBrush,CBitmap等GDI对象是否被正确删除SelectObject恢复旧对象然后删除新对象。AI搜索过程中是否在递归函数里大量动态分配内存如new一个走法列表而没有释放。尽量使用栈上对象或对象池。调试这类项目一个非常有效的方法是记录日志。在关键函数如MakeMove,UndoMove,EvaluateBoard的入口和出口添加日志输出记录参数和关键状态。当出现问题时查看日志文件就能快速定位到异常的步骤。MFC中可以使用TRACE宏或者输出到文件。