ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

C++与Qt实现网络国际象棋:从规则引擎到Socket对战开发实践

C++与Qt实现网络国际象棋:从规则引擎到Socket对战开发实践 1. 项目概述从棋盘到代码的旅程几年前我为了教一个朋友下国际象棋顺手用C写了个命令行版本的对战程序。当时觉得能走子、能判断将军、能认输就够用了。但很快朋友搬去了外地那个简陋的本地程序就失去了意义。这让我开始思考一个棋类游戏的核心乐趣除了策略博弈本身更在于“与人互动”。于是把本地双人对战升级为网络对战就成了一个自然而然的想法。这个“基于C的简易国际象棋双人对战程序设计”项目就是这段思考的产物。它不仅仅是一个棋盘逻辑的代码实现更是一次将经典游戏与现代网络技术结合的完整实践。这个项目适合谁呢首先当然是正在学习C尤其是已经掌握了面向对象基础想找一个综合性项目练手的同学。其次是对Qt框架感兴趣想了解如何用它构建带界面的桌面应用的朋友。最后也是最重要的是那些对网络编程Socket感到神秘又向往的开发者。通过这个项目你能把这三块知识串联起来用C实现严谨的游戏规则内核用Qt构建直观友好的图形界面再用Socket技术让两个相隔千里的人能在同一个棋盘上对弈。你会发现一个看似复杂的系统拆解后每一步都有清晰的路径可循。2. 核心架构设计三层分离与通信协议一个健壮的网络对战程序绝不能把所有代码都堆在一起。我采用的架构可以清晰地分为三层数据模型层Model、用户界面层View和网络控制层Controller/Network。这是一种经过验证的、能有效降低耦合度的设计思路。2.1 数据模型层棋盘与规则的核心这一层是程序的心脏完全独立于界面和网络。它的核心是一个ChessBoard类内部维护一个8x8的棋盘状态数组。每个格子可以放置一个ChessPiece对象或为空。ChessPiece是一个基类包含颜色白/黑、类型王、后、车、象、马、兵、位置等属性。然后通过派生类如King,Queen,Pawn等来实现各自独特的移动规则。这里的关键在于“规则验证”函数。例如bool Pawn::isValidMove(const Position from, const Position to, const ChessBoard board) const。这个函数需要判断兵是否直走一格起始位置可两格、是否斜吃子、是否有“吃过路兵”的机会等等。一个常见的坑是忘记在验证移动时检查移动后是否会导致己方王被将军。这需要在ChessBoard类中提供一个bool isInCheck(Color color)的方法用于遍历所有对方棋子看是否有能攻击到己方王位置的移动。每次尝试移动前都要模拟移动后的棋盘状态并调用此方法检查。注意规则验证是国际象棋编程中最易出错的部分。建议单独为“王车易位”、“吃过路兵”、“兵升变”这些特殊规则编写独立的测试用例确保逻辑万无一失。2.2 用户界面层Qt的优雅呈现我选择Qt作为GUI框架原因很简单跨平台、信号槽机制优雅、文档丰富。界面层的主要职责是“显示”和“交互”。一个MainWindow作为主窗口里面包含一个自定义的ChessWidget用于绘制棋盘和棋子。绘制棋盘可以使用QPainter通过循环绘制64个交替的矩形即可。棋子的绘制有两种常见方案一是使用Unicode字符如♔♚简单但美观度一般二是使用图片资源.png格式的棋子图片。我推荐后者视觉效果更好。可以将棋子图片作为Qt的资源文件.qrc嵌入然后根据棋子类型和颜色在对应格子上绘制QPixmap。交互的核心是处理鼠标事件。在ChessWidget的mousePressEvent和mouseReleaseEvent中我们需要将鼠标点击的像素坐标转换为棋盘逻辑坐标例如点击x,y点对应棋盘row, col。当玩家选中一个己方棋子时高亮显示该棋子当玩家拖动或点击目标格子时界面层不应直接修改模型而是应该通过信号signal通知控制层“用户试图从A1移动到B3”。2.3 网络控制层Socket连接与消息协议这是实现网络对战的关键。我们使用TCP Socket来保证数据传输的可靠和有序。程序需要有两种角色服务器Host和客户端Client。通常先启动的一方作为服务器等待连接后启动的一方作为客户端主动连接服务器的IP和端口。网络层的核心职责是收发“消息”。我们不能直接发送C对象必须定义一套简单的应用层协议。这里我设计了一个基于文本的协议每条消息以换行符\n结束易于调试。例如MOVE A2 A4表示移动棋子从A2到A4。CHAT Hello there!表示发送聊天内容。RESIGN表示认输。DRAW?表示提和。DRAW_ACCEPT或DRAW_REJECT回应和棋请求。网络控制层例如一个NetworkManager类需要维护一个QTcpSocket对象。它监听来自界面层的信号如用户走了一步棋发出moveMade(Position, Position)信号然后将其格式化为协议消息发送给对方。同时它也需要异步地读取来自网络套接字的数据一旦收到完整消息就解析其含义并通过信号例如receivedMove(Position, Position)通知给游戏逻辑控制层。实操心得处理Socket的异步读写时一定要考虑数据粘包问题。因为TCP是流式协议一次read可能收到半条消息也可能收到多条。通用的做法是建立一个缓冲区不断读取数据并存入缓冲区然后检查缓冲区中是否有完整的消息即是否包含换行符\n有则取出处理剩下的数据留在缓冲区等待下次读取。3. 开发环境搭建与工具选型工欲善其事必先利其器。一个顺畅的开发环境能极大提升效率避免在配置上浪费过多时间。3.1 C编译器与构建工具在Windows上最省心的选择是安装MSYS2通过它安装MinGW-w64工具链。这提供了g编译器和make工具。你也可以使用Visual Studio的MSVC编译器但在与Qt集成时需要注意编译器版本ABI的匹配。对于跨平台项目MinGW-w64或Linux/macOS上的Clang是更通用的选择。我强烈推荐使用CMake作为构建系统而不是Qt自带的qmake。CMake的语法更现代功能更强大并且是业界的标准。一个基本的CMakeLists.txt需要做以下几件事设置C标准如set(CMAKE_CXX_STANDARD 17)。使用find_package查找Qt6或Qt5的核心模块Core, Gui, Widgets, Network。将你的源文件添加到可执行目标。使用target_link_libraries将目标链接到Qt的库和必要的系统库。3.2 Qt框架安装与配置前往Qt官网下载在线安装器。安装时选择与你编译器匹配的Qt版本例如Qt 6.5.x MinGW 64-bit。务必勾选以下组件Qt Creator一个优秀的跨平台IDE对Qt开发支持极好。你所需的Qt模块至少包括Qt Core, Qt GUI, Qt Widgets, Qt Network。如果你倾向于使用VSCode则需要额外配置。首先安装C/C扩展和Qt相关的扩展如“Qt Configure”。关键在于配置c_cpp_properties.json文件正确添加Qt的include路径。例如在Windows上路径可能类似于C:/Qt/6.5.1/mingw_64/include。同时你还需要在CMake配置中指定Qt的安装路径。虽然VSCodeCMakeQt是可行的但对于Qt新手Qt Creator在项目创建、UI设计Qt Designer集成、调试等方面提供了开箱即用的体验能让你更专注于代码本身。3.3 辅助工具与调试技巧Git务必使用版本控制。从项目第一天就开始提交清晰地记录你的开发过程。Qt Designer这是Qt Creator内置的可视化UI设计工具。你可以通过拖拽的方式设计主窗口、对话框生成.ui文件。程序运行时Qt的uic工具会将其编译为C代码。对于棋盘这种高度定制化的绘制我们仍然需要在代码中用QPainter绘制但其他部分如按钮、菜单、状态栏完全可以借助Designer快速搭建。调试熟练使用Qt Creator或VSCode的调试器。对于网络问题一个非常实用的技巧是在发送和接收消息的函数入口处使用qDebug() “Send:” message;和qDebug() “Received:” message;打印日志。这能让你清晰地看到网络对话的全过程是排查协议错误或逻辑错误的最直接手段。4. 核心模块实现详解有了清晰的架构和准备好的工具我们就可以开始动手实现各个核心模块了。让我们深入到代码层面看看关键部分如何构建。4.1 棋盘数据模型与规则引擎实现首先定义基础的数据结构// position.h struct Position { int row; // 0-7, 对应棋盘第1-8行 int col; // 0-7, 对应棋盘a-h列 bool operator(const Position other) const { return row other.row col other.col; } bool isValid() const { return row 0 row 8 col 0 col 8; } }; // piece.h enum class PieceType { King, Queen, Rook, Bishop, Knight, Pawn, None }; enum class Color { White, Black }; class ChessPiece { public: virtual ~ChessPiece() default; Color getColor() const { return color_; } PieceType getType() const { return type_; } // 纯虚函数由子类实现特定移动规则 virtual bool isValidMove(const Position from, const Position to, const ChessBoard board) const 0; // 虚函数某些棋子如王、车需要记录是否移动过 virtual bool hasMoved() const { return false; } virtual void setMoved(bool moved) { /* 默认空实现 */ } protected: ChessPiece(Color color, PieceType type) : color_(color), type_(type) {} Color color_; PieceType type_; };以Pawn兵为例其移动规则最为复杂// pawn.cpp bool Pawn::isValidMove(const Position from, const Position to, const ChessBoard board) const { int direction (color_ Color::White) ? 1 : -1; // 白方向下行号增加黑方向上行号减少 int startRow (color_ Color::White) ? 1 : 6; // 白兵起始第2行(row1)黑兵第7行(row6) // 1. 基本前移一格 if (to.col from.col to.row from.row direction) { return board.isEmptyAt(to); // 目标格必须为空 } // 2. 起始位置前移两格 if (from.row startRow to.col from.col to.row from.row 2 * direction) { Position middlePos{from.row direction, from.col}; return board.isEmptyAt(middlePos) board.isEmptyAt(to); // 路径和目标格都必须为空 } // 3. 斜吃子 if (abs(to.col - from.col) 1 to.row from.row direction) { // 目标格有对方棋子可以吃 if (!board.isEmptyAt(to) board.pieceAt(to)-getColor() ! color_) { return true; } // 吃过路兵 const ChessPiece* lastMovedPiece board.getLastMovedPiece(); if (lastMovedPiece lastMovedPiece-getType() PieceType::Pawn) { Position lastMoveFrom board.getLastMoveFrom(); Position lastMoveTo board.getLastMoveTo(); // 判断条件对方兵上次移动了两格并且现在停在了我方的“路过”位置 if (abs(lastMoveTo.row - lastMoveFrom.row) 2 lastMoveTo.row from.row // 对方兵停在我方兵的同一行 lastMoveTo.col to.col // 对方兵停在我方要吃的位置的列 board.isEmptyAt(to)) { // 目标格是空的吃的是路过的位置 return true; } } } return false; }ChessBoard类需要管理棋盘状态、当前行动方、历史记录用于悔棋、和棋判定以及最重要的——将军检测。// chessboard.h class ChessBoard { public: ChessBoard(); void reset(); // 重置到初始局面 bool makeMove(const Position from, const Position to); // 尝试走子包含所有规则检查和状态更新 bool isInCheck(Color color) const; bool isCheckmate(Color color) const; // 判断是否被将死 bool isStalemate(Color color) const; // 判断是否僵局无子可动且未被将军 private: std::arraystd::arraystd::unique_ptrChessPiece, 8, 8 grid_; Color currentPlayer_ Color::White; Position whiteKingPos_, blackKingPos_; // ... 其他状态如易位权、吃过路兵目标格等 };makeMove函数的实现逻辑是规则引擎的核心边界检查from和to坐标是否有效。棋子存在性检查from位置是否有棋子是否属于当前行棋方基本规则检查调用piece-isValidMove(from, to, *this)。模拟移动创建一个棋盘的临时副本执行移动。将军检查在临时副本上调用isInCheck(currentPlayer_)检查移动后己方王是否被将军。如果是则移动非法。执行移动更新真实棋盘状态交换行棋方更新王的位置如果是王移动更新棋子“已移动”状态记录历史等。特殊处理如果是兵移动到底线触发升变流程如果移动完成了“吃过路兵”移除对方的兵。4.2 Qt图形界面与用户交互ChessWidget继承自QWidget负责绘制和交互。// chesswidget.h class ChessWidget : public QWidget { Q_OBJECT public: explicit ChessWidget(QWidget *parent nullptr); void setBoard(const ChessBoard* board); // 关联数据模型 void setNetworkManager(NetworkManager* manager); // 关联网络层 signals: void moveRequested(Position from, Position to); // 用户尝试走子 protected: void paintEvent(QPaintEvent *event) override; void mousePressEvent(QMouseEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override; private: const ChessBoard* board_ nullptr; NetworkManager* networkManager_ nullptr; Position selectedPos_; // 当前选中的棋子位置 QPixmap piecePixmaps_[2][6]; // 存储黑白两色六种棋子的图片 // ... 其他成员如棋盘格大小、边距等 };在paintEvent中我们进行绘制void ChessWidget::paintEvent(QPaintEvent*) { QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing); int cellSize std::min(width(), height()) / 8; // 绘制棋盘格子 for (int row 0; row 8; row) { for (int col 0; col 8; col) { QRect rect(col * cellSize, row * cellSize, cellSize, cellSize); QColor color ((row col) % 2 0) ? QColor(240, 217, 181) : QColor(181, 136, 99); painter.fillRect(rect, color); // 如果该位置被选中高亮显示 if (selectedPos_.isValid() selectedPos_.row row selectedPos_.col col) { painter.setPen(QPen(Qt::blue, 3)); painter.drawRect(rect.adjusted(2, 2, -2, -2)); } } } // 绘制棋子 if (board_) { for (int row 0; row 8; row) { for (int col 0; col 8; col) { const auto piece board_-pieceAt({row, col}); if (piece piece-getType() ! PieceType::None) { int colorIndex (piece-getColor() Color::White) ? 0 : 1; int typeIndex static_castint(piece-getType()); painter.drawPixmap(col * cellSize, row * cellSize, cellSize, cellSize, piecePixmaps_[colorIndex][typeIndex]); } } } } }鼠标事件处理逻辑void ChessWidget::mousePressEvent(QMouseEvent *event) { int cellSize std::min(width(), height()) / 8; int col event-pos().x() / cellSize; int row event-pos().y() / cellSize; Position clickedPos{row, col}; if (!clickedPos.isValid()) return; if (!selectedPos_.isValid()) { // 第一次点击选中一个棋子 if (board_ board_-pieceAt(clickedPos) board_-pieceAt(clickedPos)-getColor() board_-currentPlayer()) { selectedPos_ clickedPos; update(); // 触发重绘显示高亮 } } else { // 第二次点击尝试移动 emit moveRequested(selectedPos_, clickedPos); selectedPos_ {}; // 清空选择 update(); } }MainWindow则负责组装一切创建ChessBoard、NetworkManager和ChessWidget并将它们连接起来。它还需要处理菜单栏新游戏、认输、提和、状态栏显示当前行棋方、游戏状态和聊天窗口。4.3 网络通信模块的构建NetworkManager类封装了所有的Socket操作。// networkmanager.h class NetworkManager : public QObject { Q_OBJECT public: enum class Role { None, Server, Client }; NetworkManager(QObject *parent nullptr); bool startAsServer(quint16 port); bool connectToServer(const QString hostAddress, quint16 port); void sendMove(const Position from, const Position to); void sendChat(const QString message); // ... 其他消息发送函数 signals: void connected(); void disconnected(); void moveReceived(Position from, Position to); void chatReceived(const QString message); void gameOverReceived(GameResult result); void errorOccurred(const QString errorString); private slots: void onReadyRead(); void onConnected(); void onDisconnected(); void onErrorOccurred(QAbstractSocket::SocketError error); private: void processMessage(const QByteArray message); QTcpServer* tcpServer_ nullptr; QTcpSocket* tcpSocket_ nullptr; Role role_ Role::None; QByteArray readBuffer_; // 用于处理粘包问题的缓冲区 };服务器启动和客户端连接的实现bool NetworkManager::startAsServer(quint16 port) { tcpServer_ new QTcpServer(this); if (!tcpServer_-listen(QHostAddress::Any, port)) { emit errorOccurred(tcpServer_-errorString()); return false; } connect(tcpServer_, QTcpServer::newConnection, this, [this]() { if (tcpSocket_) { // 简易处理只允许一个连接 QTcpSocket* incomingSocket tcpServer_-nextPendingConnection(); incomingSocket-close(); delete incomingSocket; return; } tcpSocket_ tcpServer_-nextPendingConnection(); setupSocketConnection(); role_ Role::Server; emit connected(); }); return true; } bool NetworkManager::connectToServer(const QString hostAddress, quint16 port) { tcpSocket_ new QTcpSocket(this); setupSocketConnection(); tcpSocket_-connectToHost(hostAddress, port); // 连接结果通过connected()和errorOccurred()信号异步通知 role_ Role::Client; return true; // 注意这里返回true仅表示启动连接尝试成功与否看信号 } void NetworkManager::setupSocketConnection() { connect(tcpSocket_, QTcpSocket::connected, this, NetworkManager::onConnected); connect(tcpSocket_, QTcpSocket::disconnected, this, NetworkManager::onDisconnected); connect(tcpSocket_, QTcpSocket::readyRead, this, NetworkManager::onReadyRead); connect(tcpSocket_, QOverloadQAbstractSocket::SocketError::of(QTcpSocket::errorOccurred), this, NetworkManager::onErrorOccurred); }消息的读取与处理是网络层的核心void NetworkManager::onReadyRead() { while (tcpSocket_-bytesAvailable() 0) { readBuffer_.append(tcpSocket_-readAll()); // 检查缓冲区中是否有完整的消息以换行符分隔 int newlineIndex; while ((newlineIndex readBuffer_.indexOf(\n)) ! -1) { QByteArray completeMessage readBuffer_.left(newlineIndex).trimmed(); readBuffer_ readBuffer_.mid(newlineIndex 1); if (!completeMessage.isEmpty()) { processMessage(completeMessage); } } } } void NetworkManager::processMessage(const QByteArray message) { QString msg QString::fromUtf8(message); QStringList parts msg.split( ); if (parts.isEmpty()) return; if (parts[0] MOVE parts.size() 3) { Position from positionFromString(parts[1]); Position to positionFromString(parts[2]); if (from.isValid() to.isValid()) { emit moveReceived(from, to); } } else if (parts[0] CHAT) { // 消息的第一部分是“CHAT”后面是聊天内容可能包含空格 int spaceIndex msg.indexOf( ); if (spaceIndex ! -1) { QString chatMsg msg.mid(spaceIndex 1); emit chatReceived(chatMsg); } } else if (parts[0] RESIGN) { emit gameOverReceived(GameResult::Resignation); } // ... 处理其他协议命令 }5. 联调测试与常见问题排查当所有模块编码完成后真正的挑战才刚刚开始把它们整合在一起并解决必然会出现的问题。5.1 本地逻辑测试棋盘规则的“单元测试”在接入网络和界面之前必须确保棋盘逻辑本身是正确的。我编写了一系列简单的测试函数在main函数开始时调用。void runBoardTests() { ChessBoard board; board.reset(); // 测试1白兵起始可以走两格 assert(board.makeMove({1, 0}, {3, 0}) true); // A2 - A4 // 测试2黑兵相应走法 assert(board.makeMove({6, 1}, {4, 1}) true); // B7 - B5 // 测试3白兵不能直走吃子 assert(board.makeMove({3, 0}, {4, 1}) false); // A4 - B5 (无子可吃) // 测试4测试将军检测 // ... 手动摆出一个将军局面测试isInCheck是否返回true // 测试5测试王车易位条件王和车未移动、路径不被攻击、中间无子 std::cout All basic tests passed! std::endl; }使用断言assert可以帮助快速定位逻辑错误。更严谨的做法是使用像Google Test这样的单元测试框架但对于个人项目手写一些关键场景的测试也足够了。5.2 网络连接与数据同步调试网络问题是最常见的。以下是我遇到并解决过的典型问题连接失败检查防火墙是否阻止了程序监听端口服务器或发起连接客户端。在Windows上首次运行可能需要允许程序通过防火墙。确保客户端输入的服务器IP和端口号正确。可以在服务器端使用netstat -an | findstr :端口号命令查看端口是否在监听。数据收不到或不全粘包/拆包这就是为什么我们需要readBuffer_。确保你的onReadyRead函数像上面那样循环读取并按照定界符这里是\n拆分消息。一个常见的错误是认为一次readyRead信号就对应一条完整消息TCP不保证这一点。Socket Error 10053: Software caused connection abort这个错误通常意味着连接被对端异常关闭。可能的原因有程序崩溃、网络突然中断、或者你在代码中某处错误地关闭了Socket。确保关闭Socketclose()和释放内存deleteLater()的逻辑只在连接确实需要断开时执行并且避免在Socket还在使用时去操作它。状态不同步这是网络对战游戏的核心挑战。必须保证服务器是游戏状态的唯一权威。在这个点对点架构中我们实际上没有中央服务器因此双方需要保持完全一致的逻辑。任何随机性比如谁先走可以通过抛硬币决定并由一方通知另一方或非确定性操作都必须通过消息同步。例如棋局开始时双方用同一个种子初始化随机数生成器如果需要。更关键的是所有改变游戏状态的操作走子、认输都必须由一方发起通过网络通知另一方另一方在收到消息后必须在本地棋盘上执行完全相同的操作并验证其结果。如果验证失败比如收到一个非法移动则需要有错误处理机制比如断开连接并提示对方作弊或程序出错。5.3 界面与逻辑的线程安全Qt的GUI操作必须在主线程也称为UI线程中进行。而网络读写QTcpSocket在默认情况下其信号槽触发也是在创建Socket对象的线程。如果我们把NetworkManager对象创建在主线程通常是在MainWindow的构造函数里那么它的槽函数如onReadyRead也会在主线程被调用。这通常没有问题因为处理网络消息解析字符串、发出自定义信号是轻量级操作。但是如果你在onReadyRead的槽函数中直接执行了复杂的、耗时的计算比如AI思考就会阻塞主线程导致界面卡顿无响应。解决方案是将耗时的计算移到单独的线程QThread中。对于这个简易项目规则验证和棋盘状态更新很快不太需要。但如果你未来要加入AI功能就必须注意这一点。一个简单的模式是网络线程收到“MOVE”消息后通过信号通知主线程的某个控制器控制器再在UI线程中更新ChessBoard模型然后通知ChessWidget重绘。确保对共享数据如棋盘状态的访问是线程安全的必要时使用互斥锁QMutex。6. 功能扩展与性能优化思路一个基础版本完成后你可以考虑添加更多功能让它变得更实用、更强大。6.1 增强功能点悔棋功能在ChessBoard类中维护一个std::vectorMoveHistory记录每一步的移动详情起止位置、被吃的子、易位权变化等。当一方请求悔棋时发送UNDO消息给对方。对方同意后双方各自在本地回退一步棋盘状态。需要处理连续悔棋、悔棋后重新走子等边界情况。聊天与游戏状态通知我们已经定义了CHAT协议。可以在主界面添加一个QTextEdit作为聊天记录框和一个QLineEdit作为输入框。发送聊天内容就是调用networkManager-sendChat(text)。同样游戏状态如“对方正在思考...”、“将军”、“将死白方获胜”也可以通过类似的消息协议进行广播并在状态栏或一个信息框中显示。观战模式这需要改变架构。可以引入一个简单的中心服务器。对战双方连接到服务器并相互同步。观战者也连接到同一个服务器。服务器将双方的走子消息MOVE转发给所有连接的观战者。观战者的客户端程序只包含界面和棋盘逻辑不能走子只能接收和显示走子信息。简单的AI对手实现一个“人机对战”模式。这需要引入搜索算法如极小化极大算法Minimax with Alpha-Beta Pruning和局面评估函数。评估函数根据子力价值王无限大、后9分、车5分、象3分、马3分、兵1分、棋子位置、王的安全度、兵形结构等因素给棋盘打分。AI会在给定的时间内搜索若干步选择评估分数最高的走法。这是一个独立的、挑战性很高的子项目。6.2 性能与代码质量优化走法生成优化在AI搜索或穷举验证时需要频繁生成所有合法走法。暴力遍历64个格子检查是否合法效率很低。可以预先为每种棋子类型建立“移动增量表”。例如车的移动增量是(±1, 0)和(0, ±1)沿着这些方向一直走到撞子为止。这样生成走法的速度会快很多。使用Zobrist哈希实现置换表对于AI搜索同一个棋盘局面可能会通过不同的走子顺序达到。我们可以为棋盘状态计算一个几乎唯一的哈希值Zobrist Hashing并将搜索过的局面及其评估结果存储在一个哈希表置换表中。下次遇到相同局面时直接查表避免重复搜索能极大提升搜索深度和速度。引入日志系统替代散落的qDebug()使用一个简单的日志类可以按级别Info, Debug, Warning, Error输出日志到文件或控制台方便后期问题追踪。代码重构与设计模式随着功能增加代码可能会变得混乱。考虑使用更清晰的设计模式。例如用“状态模式”来管理游戏的不同状态等待连接、对局中、已结束用“命令模式”来封装每一步走子操作方便实现悔棋、重做功能用“观察者模式”来让界面组件自动响应模型数据的变化。这个项目从零开始涵盖了从底层数据结构、算法逻辑到上层UI交互、网络通信的完整开发链条。过程中你会遇到无数细节问题每一个问题的解决都是宝贵的经验。当你最终看到两个独立的程序窗口通过网络在同一个棋盘上顺畅地对弈时那种成就感是无可比拟的。它不仅仅是一个国际象棋程序更是一个理解如何构建复杂软件系统的绝佳范例。
返回列表