Qt和C++五子棋AI开发:界面设计详解

发布时间:2026/9/15 13:07:49
Qt和C++五子棋AI开发:界面设计详解 1. 为什么我要用Qt和C做这个五子棋AI先交代一下这个系列的由来。我一直想找一个既能练C功底、又能看到实际效果的项目——纯写命令行程序太枯燥做图形界面又怕陷入业务逻辑的泥潭。五子棋AI刚好卡在一个很舒服的位置规则简单、状态空间可控、胜负判定清晰但又能承载搜索算法、评估函数、界面交互这些实打实的知识点。市面上大多数五子棋AI教程只讲算法本身给你一个控制台版本黑子白子用字符打印。这其实错过了很多东西——算法跑通了但你对一个完整的软件长什么样仍然没有概念。我这个系列打算做一套完整的桌面应用用Qt做界面用C写核心逻辑从棋盘绘制到落子交互从搜索算法到评估函数一路做到底。这一篇是系列的第一篇把引言的背景讲清楚然后重点拆解界面设计部分。先回答一个很多人会问的问题为什么不直接用Unity或者HTML5做棋类游戏本质上你需要的是一个能在窗口里绘制图形、处理鼠标事件、并且不拖后腿的GUI框架。Qt在C生态里的地位不需要多说了——跨平台、信号槽机制舒服、QPainter绘图API足够直接而且它对C标准的支持很友好不会逼你用一堆宏和模板做奇技淫巧。更关键的是Qt的信号槽和事件循环非常适合处理玩家下棋、AI思考、AI落子这种带状态的交互流程。如果你已经在纠结选什么版本的Qt——我建议直接用Qt 5.15 LTS或者Qt 6的长期支持版本。五子棋这种2D应用用不到太新潮的特性稳定可控比频繁搬家重要得多。这个项目最终要实现的东西包括一个能看清棋盘的GUI、支持鼠标落子和悔棋、单人模式下AI自动应手以及两种AI强度一种用纯贪心评估一种用极大极小搜索加α-β剪枝。实际跑起来之后你能直观感受到搜索深度提高后棋力变化的差距这才是算法的魅力所在——它不是一堆数学符号而是确实让程序变聪明了。学习这个项目的最佳路径是先照着这篇文章把界面跑起来动手改改颜色、棋盘尺寸、棋子样式找到手感等后续文章把搜索算法接进来之后再回头看界面层哪些数据结构是为算法预留的。界面层和搜索层解耦这件事很多初学者不重视结果是改一个功能牵一发动全身。我会在下面的设计里始终注意这个问题。2. 界面设计的整体架构棋盘数据与控制逻辑的分离五子棋界面表面上就是一张棋盘加一堆按钮但如果代码结构设计得不好后面接搜索算法的时候会非常痛苦。我在动手写之前先把整个项目的层次想清楚了——这也是我认为本篇文章最值得仔细看的部分。2.1 核心数据结构的定义我的做法是用一个Board类来抽象棋盘这个类不关心任何界面相关的逻辑只管理一个二维数组。棋子状态用三个整数常量表示EMPTY0表示空位BLACK1表示黑子WHITE2表示白子。棋盘大小是15x15对应五子棋的标准规格。class Board { public: static constexpr int kBoardSize 15; static constexpr int kEmpty 0; static constexpr int kBlack 1; static constexpr int kWhite 2; Board() { clear(); } int getStone(int row, int col) const { if (row 0 || row kBoardSize || col 0 || col kBoardSize) { return kEmpty; } return grid_[row][col]; } bool setStone(int row, int col, int player) { if (row 0 || row kBoardSize || col 0 || col kBoardSize) { return false; } if (grid_[row][col] ! kEmpty) { return false; } grid_[row][col] player; return true; } bool isEmpty(int row, int col) const { return getStone(row, col) kEmpty; } void clear() { memset(grid_, kEmpty, sizeof(grid_)); } private: int grid_[kBoardSize][kBoardSize]; };为什么用一个独立的类而不是直接在窗口类里放一个二维数组原因是后续的AI模块需要频繁地对棋盘做假设落子、评估、撤销落子这样的操作如果棋盘状态和Qt窗口耦合在一起每次模拟搜索都要操作界面组件效率低且容易出错。把棋盘数据独立出来AI模块只需要针对Board这个纯数据类做操作完全不需要关心屏幕上的显示。2.2 界面层如何与数据层交互界面层我设计了一个MainWindow类继承自QMainWindow。它的职责是创建布局、接收鼠标事件、调用Board的接口修改数据、然后刷新显示。为了让后续的AI逻辑接入更方便我现在就把玩家落子之后要做的流程固定下来鼠标点击棋盘判断点击位置是否落在某个交叉点附近检查该位置是否合法没有棋子调用board_.setStone(row, col, currentPlayer)更新数据检查胜负刷新界面如果当前是双人模式且轮到下一位玩家切换currentPlayer如果是人机模式且轮到AI触发AI线程计算。这里要强调一个关键变化由于搜索算法本身也有原型我不能在MainWindow里写死玩家点击之后必须切换到另一边的逻辑。如果以后要加AI对战AI的观战模式或者撤销到任意一步的功能界面层必须只负责谁发出的指令、指令发到哪个模块而不关心这个指令具体怎么处理。因此我在界面层引入了一个简单的GameController角色用来协调棋子归属和回合切换class GameController : public QObject { Q_OBJECT public: enum class GameMode { HumanVsHuman, HumanVsAI }; explicit GameController(QObject* parent nullptr); void startNewGame(GameMode mode); bool tryPlaceStone(int row, int col); void undoLastMove(); signals: void stonePlaced(int row, int col, int player); void gameFinished(int winner); void stateChanged(); private: Board board_; int currentPlayer_; GameMode gameMode_; QListQPairint, int moveHistory_; };这个控制器的名字有点唬人但逻辑其实很简单它手里攥着棋盘数据、当前该谁走、走棋历史的栈。玩家点击界面后MainWindow不是直接改数组而是调用controller.tryPlaceStone(row, col)由控制器决定这个落子合法不合法、要不要发信号通知界面刷新。这样做的好处是之后AI要落子也只要调用controller.tryPlaceStone就完事了——AI模块不需要知道任何窗口相关的细节。有很多Qt初学者习惯把所有东西都塞进MainWindow虽然小程序也能跑但后面一旦加入多线程AI搜索、动画回放、复盘导出的功能那个类会膨胀到几千行没法维护。从这个系列第一篇就把层次理清楚后面会越写越顺。2.3 为什么15x15的棋盘尺寸是一个合理的选择五子棋的棋盘规格其实有不同的流派19x19围棋棋盘街头公园那种、15x15主流比赛规格、甚至有人用13x13来加速AI测试。选择15x15有以下几个实际原因15x15正好在计算机能处理和人类视觉舒适之间达到平衡。棋盘太大会显得棋子稀疏太小则连珠太容易。极大极小搜索的复杂度与棋盘大小直接相关。15x15的空位总数是225虽然搜索过程中会不断限制候选点范围但更大棋盘会让剪枝压力显著增大。对于教学用途15x15既能体现AI的棋力又不会慢到让人失去耐心。Qt的绘图坐标计算方便。把棋盘分成15x15格画板尺寸做成600x600的话每个格子正好40像素计算交叉点坐标就是简单的乘除不用考虑浮点数误差。还有一个实际的考虑后续如果要写评估函数比如判断某一点在横、竖、两个斜方向上的连子情况和威胁程度15x15的边界条件处理比19x19更省心。我见过不少用19x19棋盘写AI的搜索深度一到4层就开始卡顿。这不是说19x19不行而是你需要更复杂的优化。作为入门到进阶的过渡项目15x15是性价比之王。3. 核心绘制与交互从画格子到落子响应的完整链路界面模块最重要的部分是绘制和鼠标交互。这两个东西一个管显示一个管输入必须配合好否则会出现点上去的棋子和视觉位置对不上的尴尬情况。我把这部分代码完整拆开来讲。3.1 棋盘绘制QPainter的使用细节Qt里自定义控件的第一课就是重写paintEvent。我先定义了一个BoardView类继承自QWidget专门负责棋盘的绘制。这样写的好处是界面的其他部分工具栏、状态栏不用关心绘图细节统一由这个控件管理重绘。class BoardView : public QWidget { Q_OBJECT public: explicit BoardView(QWidget* parent nullptr); void setController(GameController* controller); void updateBoard(); protected: void paintEvent(QPaintEvent* event) override; void mousePressEvent(QMouseEvent* event) override; private: QPointF gridToPixel(int row, int col) const; bool pixelToGrid(const QPointF pos, int row, int col) const; GameController* controller_; int margin_; int cellSize_; };构造函数中我初始化了棋盘边距和格子大小。margin_是棋盘边缘留白cellSize_是每个格子的像素间距。绘制棋盘时先用drawLine画出15x15的网格线然后绘制星位标记天元和四个星位最后遍历棋盘数组把已有棋子画上去。void BoardView::paintEvent(QPaintEvent* event) { QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing, true); // 背景颜色 painter.fillRect(rect(), QColor(222, 184, 135)); // 木纹色 // 画网格 painter.setPen(QPen(QColor(80, 60, 40), 1.2)); for (int i 0; i 15; i) { // 横线 painter.drawLine(margin_ i * cellSize_, margin_, margin_ i * cellSize_, margin_ 14 * cellSize_); // 竖线 painter.drawLine(margin_, margin_ i * cellSize_, margin_ 14 * cellSize_, margin_ i * cellSize_); } // 画星位 QVectorQPairint, int stars {{3, 3}, {3, 11}, {11, 3}, {11, 11}, {7, 7}}; painter.setBrush(Qt::black); painter.setPen(Qt::NoPen); for (const auto star : stars) { QPointF center gridToPixel(star.first, star.second); painter.drawEllipse(center, 3.5, 3.5); } // 画棋子 if (controller_) { for (int r 0; r 15; r) { for (int c 0; c 15; c) { int stone controller_-getBoard().getStone(r, c); if (stone Board::kEmpty) continue; drawStone(painter, r, c, stone); } } } }drawStone是单独的辅助函数用来画黑白棋子。我用径向渐变模拟棋子的立体感——纯圆形不加渐变会显得像硬币贴纸加了渐变之后观感会好很多void BoardView::drawStone(QPainter painter, int row, int col, int player) { QPointF center gridToPixel(row, col); qreal radius cellSize_ * 0.42; QRadialGradient gradient(center.x() - radius * 0.3, center.y() - radius * 0.3, radius * 1.3, center.x(), center.y()); if (player Board::kBlack) { gradient.setColorAt(0, QColor(120, 120, 120)); gradient.setColorAt(0.3, QColor(60, 60, 60)); gradient.setColorAt(1, QColor(10, 10, 10)); } else { gradient.setColorAt(0, QColor(255, 255, 255)); gradient.setColorAt(0.5, QColor(230, 230, 230)); gradient.setColorAt(1, QColor(180, 180, 180)); } painter.setPen(QPen(Qt::black, 0.6)); painter.setBrush(gradient); painter.drawEllipse(center, radius, radius); }gridToPixel函数实现网格坐标到像素坐标的转换。交叉点索引是0到14对应的像素位置就是margin_ index * cellSize_QPointF BoardView::gridToPixel(int row, int col) const { return QPointF(margin_ col * cellSize_, margin_ row * cellSize_); }反过来鼠标点击的像素坐标要变回网格坐标。这里有一个非常容易踩坑的细节——到底取最近的交叉点还是取点所在的格子中心答案必须是最近的交叉点。因为在视觉上玩家点击的是两条线的交点而不是格子中间的空隙。bool BoardView::pixelToGrid(const QPointF pos, int row, int col) const { qreal x pos.x() - margin_; qreal y pos.y() - margin_; if (x -cellSize_ / 2.0 || y -cellSize_ / 2.0) { return false; } int colIndex qRound(x / cellSize_); int rowIndex qRound(y / cellSize_); if (rowIndex 0 || rowIndex 15 || colIndex 0 || colIndex 15) { return false; } row rowIndex; col colIndex; return true; }这里用qRound做四舍五入取整。判断条件是点到交叉点的距离是否超过半格如果超过就说明点在了棋盘外。这个函数的边界条件容易被忽略我调试的时候发现如果不加x -cellSize_ / 2这行判断点击棋盘左侧边缘外不远的位置会被错误地映射到0列产生隔空落子的怪异表现。3.2 鼠标落下棋子后的完整处理流程鼠标点击事件本身很简单难的是点击之后要走的那一整条链路。我不想让BoardView直接修改棋盘数据而是通过控制器统一处理void BoardView::mousePressEvent(QMouseEvent* event) { if (event-button() ! Qt::LeftButton) { QWidget::mousePressEvent(event); return; } int row -1, col -1; if (!pixelToGrid(event-pos(), row, col)) { return; } if (controller_ !controller_-tryPlaceStone(row, col)) { return; } update(); // 触发重绘 }控制器里的tryPlaceStone是核心方法。我在这里还加了一些状态判断比如游戏是否结束、当前是否允许落子bool GameController::tryPlaceStone(int row, int col) { if (gameOver_) return false; if (gameMode_ GameMode::HumanVsAI currentPlayer_ ! Board::kBlack) { return false; // 人机模式下只允许玩家下黑棋AI下白棋 } if (!board_.setStone(row, col, currentPlayer_)) { return false; } moveHistory_.append(qMakePair(row, col)); emit stonePlaced(row, col, currentPlayer_); if (checkWin(row, col, currentPlayer_)) { gameOver_ true; emit gameFinished(currentPlayer_); return true; } // 检查平局 if (moveHistory_.size() Board::kBoardSize * Board::kBoardSize) { gameOver_ true; emit gameFinished(Board::kEmpty); return true; } // 切换玩家 currentPlayer_ (currentPlayer_ Board::kBlack) ? Board::kWhite : Board::kBlack; emit stateChanged(); return true; }checkWin是五子棋界面的一个核心逻辑大家通常会想到扫四个方向。我在这里用了(dx, dy)方向数组把八方向归并成四个方向轴bool GameController::checkWin(int row, int col, int player) const { const int directions[4][2] {{0, 1}, {1, 0}, {1, 1}, {1, -1}}; for (const auto dir : directions) { int count 1; // 正方向 for (int step 1; step 5; step) { int nr row dir[0] * step; int nc col dir[1] * step; if (board_.getStone(nr, nc) player) { count; } else { break; } } // 反方向 for (int step 1; step 5; step) { int nr row - dir[0] * step; int nc col - dir[1] * step; if (board_.getStone(nr, nc) player) { count; } else { break; } } if (count 5) { return true; } } return false; }这个实现有一个需要留意的点getStone对越界坐标返回kEmpty所以不需要在循环里额外做边界判断更简洁。这也是我在定义Board时把越界情况想办法兜住的原因——上层逻辑可以放心调用不用到处堆if判断。3.3 界面重绘的性能分析与优化空间初版代码我直接用了全盘重绘每次鼠标点击都遍历整个15x15棋盘把棋子全部画一遍。这样在五子棋这种低频率交互场景下毫无压力执行一次paintEvent耗时不超过1毫秒。不过如果你以后想把项目扩展成落子动画或AI思考过程可视化全盘重绘就不够用了。一个简单有效的优化是引入脏矩形机制只在落子位置周围的局部区域触发重绘。Qt里QWidget::update(const QRect)允许你传入需要更新的矩形区域系统会把多个更新请求合并成一次重绘。比如落子在(row, col)可以这样通知void BoardView::notifyStoneChanged(int row, int col, int player) { QPointF center gridToPixel(row, col); QRectF dirtyRect(center.x() - cellSize_, center.y() - cellSize_, cellSize_ * 2, cellSize_ * 2); update(dirtyRect.toRect()); }不过实话说对五子棋这个项目在性能优化上投入太多时间意义不大。我见过一篇文章把棋盘绘制优化到极致用了QOpenGLWidget帧率飙到上千但玩家一秒钟最多落一两个子这显然是过度工程。正确的态度是先写出清晰可靠的代码性能分析工具指出问题再优化。4. 界面功能扩展悔棋、新游戏、提示与规则校验界面不能只有一块棋盘一个完整的应用还应该提供操作入口和信息反馈。我做了右侧的工具栏面板用QVBoxLayout排列了几个核心控件。这里面的功能虽然都不复杂但它们共同构成了一个能真正拿来下棋的软件的体验基础。4.1 新游戏与游戏模式切换的实现新游戏按钮对应控制器的startNewGame方法void GameController::startNewGame(GameMode mode) { board_.clear(); moveHistory_.clear(); currentPlayer_ Board::kBlack; gameOver_ false; gameMode_ mode; emit stateChanged(); }我在工具栏里放了两个单选按钮QRadioButton一个是双人对战一个是人机对战。选择模式只是调用startNewGame并传入不同的枚举值逻辑上非常清晰。值得注意的一点是切换模式时不能直接改变一个成员变量就完事——必须清除棋盘并重置所有状态。我最初犯过一个错误在游戏进行中切换到人机模式结果棋盘上还有旧的残局AI拿这个残局继续算行为非常诡异。后来统一改成模式切换本质就是重新开一局玩家之前下的那盘棋不保留。4.2 悔棋与历史记录为什么需要moveHistory_悔棋功能的实现依赖于我们之前定义的moveHistory_列表这是一个QListQPairint, int按顺序记录每一步落子。悔棋时只需要弹掉最后一步清掉对应的棋盘位置然后把回合状态恢复一下bool GameController::undoLastMove() { if (moveHistory_.isEmpty()) return false; if (gameOver_) return false; // 游戏结束后不允许悔棋 auto lastMove moveHistory_.takeLast(); board_.setStone(lastMove.first, lastMove.second, Board::kEmpty); currentPlayer_ (currentPlayer_ Board::kBlack) ? Board::kWhite : Board::kBlack; emit stateChanged(); return true; }这里有一个细节要注意悔棋后currentPlayer_的处理和人机模式绑定。在人机模式下如果玩家是黑棋、AI是白棋那么玩家下了一步黑棋轮到AI白棋玩家悔棋这一步被撤销棋盘上少一颗黑子此时该谁下答案是应该回到玩家准备下黑棋的状态也就是currentPlayer_要切回Board::kBlack。上面的代码在悔棋后总是切换玩家正好覆盖了这个场景不需要额外判断模式。如果你想支持连续悔棋两步先把AI的白棋撤销再把自己的黑棋撤销只需要连续点两次悔棋按钮就能从轮到玩家回到轮到AI再回到轮到玩家。这个逻辑我自己测试下来是自洽的。4.3 胜负判定后的状态锁与信息提示游戏结束后需要一个状态锁。所谓状态锁就是用一个gameOver_布尔标志阻止进一步的落子和悔棋操作。我在tryPlaceStone和undoLastMove里都检查了这个标志这样即使玩家反复点击棋盘也不会产生无效修改。界面上的反馈我用状态栏QStatusBar来显示当前的回合信息和游戏结果。在MainWindow里连接控制器的信号connect(controller_, GameController::stateChanged, this, MainWindow::updateStatusBar); connect(controller_, GameController::gameFinished, this, MainWindow::handleGameFinished);updateStatusBar根据当前的currentPlayer_显示轮到黑棋或轮到白棋handleGameFinished弹出一个消息框告知获胜方并且把状态栏同步更新。这样玩家对游戏状态一目了然不会出现我不知道该谁走的困惑。4.4 落子提示功能一个最容易忽略的微优化为了让UI体验更完善我还加了一个高亮最后一步落子的辅助功能——在最后一颗棋子上画一个红色小标记让玩家快速定位最近的落子位置。这个功能难度不高但对实际对局的帮助很大尤其是AI在角落落子的时候你不需要满棋盘找那颗新下的子。实现的思路是在BoardView里缓存一个lastMove_变量刷新时在对应的棋子旁边画一个小红点void MainWindow::onStonePlaced(int row, int col, int player) { Q_UNUSED(player); boardView_-setLastMove(row, col); boardView_-update(); }绘制时在drawStone之后追加一小段逻辑if (lastMoveRow_ row lastMoveCol_ col) { painter.setBrush(QColor(220, 40, 40)); painter.setPen(Qt::NoPen); qreal offsetX center.x() radius * 0.7; qreal offsetY center.y() - radius * 0.7; painter.drawEllipse(QPointF(offsetX, offsetY), 3, 3); }红点位置选在棋子的右上角避免和棋子本身的圆形视觉重心重叠。5. 界面开发过程中的几个容易翻车的细节这一段我集中写一下整个界面开发过程中实际踩过的坑。这些坑不亲自踩一遍很难意识到但看一遍经验就能省下不少调试时间。5.1 坐标转换的玄机qRound vs. int强制转换最早的测试版本里我用的坐标转换是int colIndex (x / cellSize_);这种整数除法的行为是向下取整。设想你点击了棋盘第二个交叉点附近像素坐标可能是100而cellSize_是40100 / 40 2.5int(2.5) 2看起来没问题。但你点击第二个交叉点左侧一点点比如坐标8181 / 40 2.025取整后变成2对上了。但如果你点击第二个交叉点右侧很远的位置坐标119119 / 40 2.975结果还是2。这会导致你想点第3个交叉点时系统可能判定为第2个或第3个进而把子落在偏离你视觉位置的地方。正确做法是使用qRound四舍五入取整让点击位置落在两个交叉点正中间的时候取最近的一个。简单说int(x / 40)是点在第几个区间内以线段的左端为基准qRound(x / 40.0)是点到哪个端点的距离更近以端点为中心做邻域归属。这两者在视觉上有天壤之别。我建议所有需要做精确坐标转换的绘图控件一律用qRound。5.2 Qt高DPI显示下的模糊问题现在很多笔记本默认开启了缩放显示125%或150%如果你直接运行Qt程序界面可能会显得模糊或控件错位。解决办法是在main函数最开始加上高DPI适配的声明并且放到QApplication实例化之前int main(int argc, char *argv[]) { #if QT_VERSION QT_VERSION_CHECK(5, 6, 0) QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); #endif QApplication app(argc, argv); ... }如果你是在双屏不同缩放比的场景下工作还要额外处理AA_UseHighDpiPixmaps。这一点对体验的影响很大——你辛辛苦苦绘制的渐变棋子如果因为没有开启高DPI适配而被模糊处理视觉质感大打折扣。5.3 绘制性能的误区不要提前优化我在网上看到过很多Qt绘图教程一开始就教人做双缓冲绘图实际上Qt的QWidget在默认情况下就已经开启了双缓冲。paintEvent里直接绘制即可不需要你手动创建QPixmap做一次离屏渲染再贴上去。手动双缓冲在很多场景下反而会拖慢速度——因为你要额外做一次像素拷贝。正确的优化顺序是先写清晰的绘制代码运行起来观察是否流畅卡顿的话用QElapsedTimer定位耗时在哪个环节再针对性地优化。对于五子棋这个项目我拍胸脯告诉你直接绘制完全流畅。5.4 中文编码与国际化准备Qt 5及以上版本源码里直接写中文字符串是没问题的源文件保存成UTF-8编译器就能正确处理。但如果你在Windows上用了MSVC编译器且系统区域设置为中文QString和UTF-8之间有时会出现乱码。我的经验是统一在项目文件里加上#if _MSC_VER #pragma execution_character_set(utf-8) #endif这样做能保证MSVC把字符串字面量按UTF-8处理。另一个更规范的做法是界面里所有用户可见的字符串都通过tr()包一层并单独维护翻译文件。Qt自带的lupdate和lrelease工具可以把tr()包裹的字符串导出成.ts文件后续需要做英文版或日文版界面时直接翻译即可。代码里养成用tr()的习惯虽然前期有点麻烦但对项目长期发展非常有利。5.5 棋盘控件大小与窗口缩放的匹配最后一个常见的体验问题是窗口缩放的时候棋盘控件会变形。QWidget默认情况下会跟随父窗口缩放拉伸如果BoardView被拉伸成非正方形绘制出来的棋盘网格就会变形——格子变成长方形棋子变成椭圆。解决方案有两种在BoardView::resizeEvent里强制保持正方形void BoardView::resizeEvent(QResizeEvent* event) { QWidget::resizeEvent(event); int side qMin(width(), height()); resize(side, side); }或者使用QGraphicsView场景来管理绘制的比例缩放这样可以让棋盘始终填满控件并且保持宽高比。但QGraphicsView的学习成本更高本项目的棋盘尺寸是固定的方案一已经够用。我把窗口中央布局稍微调整了一下棋盘控件固定大小周围加一个弹簧把空白区域吸收掉。这样无论如何拉伸主窗口棋盘都保持方正界面的观感也更稳。6. 到这里能玩到什么程度现阶段的功能验证与下一步预告目前这篇做完你手上的程序已经具备以下能力在15x15的棋盘上用鼠标交替落子黑白双方轮流正确的胜负判定五颗连珠出现后弹出提示支持悔棋支持重新开始新游戏支持双人模式和单人模式切换单人模式下当前是玩家执黑手动落子AI部分先空着界面支持高DPI显示缩放窗口不会导致棋盘变形。我建议你拿到代码后先做以下几件事来加深理解改一改cellSize_和margin_感受棋盘整体大小变化对视觉的影响给棋子换一个颜色方案比如用红蓝棋子替代黑白棋子看看渐变绘制代码能不能直接复用把checkWin改成获胜时高亮获胜的五颗棋子这会加深你对方向数组的理解在paintEvent里加一行qDebug() repaint观察每次落子后绘制函数的调用次数。下一篇文章会进入这个项目的核心区——极大极小搜索和α-β剪枝算法。我会先带着你手写一个不带剪枝的极大极小搜索原型让你感受到纯暴力搜索有多慢然后再逐步加上α-β剪枝、启发式候选点排序和评估函数。界面层在这篇文章打下的数据结构和控制器基础到那时会直接发挥作用。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询