简介:这是一份基于Qt框架实现的六子棋(Connect6)对战项目源码,专为计算机相关专业学生完成课程设计、期末大作业而准备,也适合希望进行Qt项目实战练习的中初级学习者。源码实现完整的C++逻辑与界面布局,共30个文件,涵盖4个cpp源文件、4个头文件、UI界面定义、项目配置文件,以及9张界面效果图、7个wav音效素材,压缩包整体约17MB,资源类型覆盖源码、界面与多媒体,结构清晰,可直接导入Qt Creator进行编译运行。项目整体获得98分的高分评价,说明其完成度、规范性与运行效果均有较好保障。当前已有810人学习下载,既可作为课程设计的直接参考,也能作为二次开发的基础,帮助读者理解Qt事件响应、主界面与游戏界面切换、棋盘绘制及胜负判断等核心模块的整合方式。 先多说一句:这份六子棋源码zip我拿到手之后,第一反应不是打开pro文件,而是先把整个包解压在本地,花了几分钟把目录结构和资源文件都过了一遍。因为说实话,六子棋这种带完整界面和AI逻辑的Qt教学项目,下载下来最容易出问题的不是代码本身,而是环境不对、工程配置缺失、或者资源路径写死导致的运行崩溃。这篇博文我会从怎么让工程正常跑起来开始,一路拆到核心算法和界面交互的实现思路,最后再说编译发布时的那些坑。不管是刚接触Qt的新手,还是想找个实战项目练手的老手,都能从中拿到点东西。
1. 解压源码后第一步:先理清工程结构,别急着点运行
拿到任何Qt项目源码zip,我个人的习惯都是先看目录结构,再看依赖,最后才点开Qt Creator编译。原因很简单:Qt项目不像普通脚本,它依赖模块、qrc资源文件、编译器版本,有时候原作者用了你没装的模块,编译报错会直接劝退。这个六子棋项目我解压后,主要文件包括pro工程文件、main.cpp入口、MainWindow窗口类、棋盘绘制类、AI逻辑类,以及game logic相关代码和qrc资源文件。整个结构对教学项目来说很典型,没有冗余。
1.1 环境选择和pro工程配置检查
用Qt开发桌面棋类游戏,我强烈建议先固定在Qt 5.15.2 LTS或者Qt 6.2以上版本。五子棋、六子棋这类程序主要依赖Qt Widgets模块,几乎用不到什么高级特性,所以版本兼容性其实很好。真正会卡住你的是编译器套件,Windows上建议用MinGW 64-bit,不要混用MSVC和MinGW编译的第三方库,不然链接的时候一堆莫名其妙的undefined reference。
打开pro文件之后,重点看有没有下面几行基础配置:
QT += core gui widgets CONFIG += c++11 TARGET = SixInARow TEMPLATE = app SOURCES += main.cpp MainWindow.cpp BoardWidget.cpp GameLogic.cpp AIPlayer.cpp HEADERS += MainWindow.h BoardWidget.h GameLogic.h AIPlayer.h如果项目中用到网络对战,还需要加network模块。确认pro里没有多余的、你没安装的模块——很多下载下来的源码就是在这里出问题。改完pro文件后,我习惯先执行qmake再编译,而不是直接Ctrl+R全自动。虽然多一步,但能清楚看到qmake阶段有没有报配置错误,好排查。
1.2 资源文件与路径问题
解压zip之后,如果发现运行界面上的背景图、棋子纹理全是空白,多半是qrc文件路径和图片存放位置不对。qrc资源系统是Qt相对不容易踩坑、但一旦踩了就很难受的点。最好打开qrc文件看一眼前缀(prefix)是不是 /,引用的图片路径是否与实际资源目录一致。如果源码是Windows环境下写的图片路径用反斜杠分隔,Linux下编译就会出问题。我自己的习惯是全部改用正斜杠,qrc的路径统一用 / 开头,资源名保持小写。
这个项目我记得默认是纯代码绘制棋盘和棋子,不依赖外部图片,这样反而省心——用QPainter画棋盘比贴图更灵活,而且视觉风格可以随时调整。跑起来最怕的是闪退,如果你一运行程序就崩,先检查一下main.cpp里有没有正确设置编码:
int main(int argc, char *argv[]) { QApplication a(argc, argv); MainWindow w; w.show(); return a.exec(); }如果代码里有中文字符串且显示乱码,不是程序逻辑问题,是源码文件编码不是UTF-8。用记事本另存为UTF-8编码,或者在pro里加一行msvc: QMAKE_CXXFLAGS += /utf-8就能解决。
1.3 编译成功不代表能安静下棋
工程编译通过后,我建议先把所有对战模式都点一遍。这个六子棋项目一般会提供人机对战和双人对战两种模式,功能入口可能在菜单栏或者主界面按钮上。如果你点了某个按钮程序没反应,多半是信号槽连接出了问题,最常见的写法错误是:
connect(ui->btnStart, SIGNAL(clicked()), this, SLOT(startGame()));这种写法如果槽函数名或者参数签名不一致,编译期不报错,运行期信号发不出去。我个人强烈建议在新代码里用指针语法:
connect(ui->btnStart, &QPushButton::clicked, this, &MainWindow::startGame);好处是编译期就能检查信号和槽是否存在、参数是否匹配,能在出门前就拦住一大部分问题。
2. 棋类游戏的核心:六子棋规则建模与胜负判定算法
六子棋和五子棋最大的不同在于胜利条件变成连续六子连线,棋盘大小往往也会做得更大一些,我见到的很多教学项目用的是16x16甚至19x19。这份源码里棋盘大小是由常量控制的,这个设计很聪明,改一个数字就能在五子棋和六子棋之间切换,后续如果你想扩展成七子棋甚至N子棋,基本上零成本。棋类游戏的核心向来不是界面,而是底层的数据模型和判定逻辑,这一部分我会重点拆解。
2.1 棋盘数据模型怎么设计
棋盘可以用一个二维数组表示,int board[16][16]或者QVector<QVector<int>> board。用0表示空位,1表示黑子,2表示白子。选择数组而不是自定义类,是因为棋类逻辑用最简单直接的数据结构反而效率最高、最不容易出bug。每次搜索棋型时,频繁读写二维数组的代价最低,而且不用处理智能指针所有权问题。
源码里我注意到还额外维护了一个lastMove变量,保存最后一手棋的坐标。这个变量用处很大:悔棋的时候直接定位到最后一手,高亮当前棋子的时候也要用到,胜负判定的时候还能从最后一手开始搜索而不是全盘扫描,性能节省不少。如果你自己写,我建议保留这个设计。
2.2 胜负判定:八方向连续棋子统计
六子棋的胜负判定,核心就是判断某个点向八个方向扩展,每个方向上同色棋子的连续长度,只要任意一个方向上的连续同色棋子数大于等于6,判胜。实际上在五子棋中方向通常简化成四个轴(横、竖、左斜、右斜),因为每个轴两端都要看,这一个轴就覆盖了两个方向。源码里的实现思路也是四个轴:
bool GameLogic::checkWin(int row, int col, int player) { int directions[4][2] = {{0,1}, {1,0}, {1,1}, {1,-1}}; for (auto &dir : directions) { int count = 1; count += countDirection(row, col, dir[0], dir[1], player); count += countDirection(row, col, -dir[0], -dir[1], player); if (count >= 6) return true; } return false; }countDirection函数从 row,col 位置沿方向逐个检查,遇到不同色棋子或者越界就停止,返回连续的棋子数。这个函数是胜负判断里最基础也最重要的工具函数,AI下棋时也要反复用到它来评估局势,务必好好研究。
2.3 一个加速技巧:只检查落子点
初学者容易犯的错是每次落子后全棋盘遍历判断胜负。如果棋盘是16x16,每次全盘扫的代价虽然也不大,但游戏逻辑被塞进了无关循环,可读性差。更合理的方案是:每次落子后,只从落子位置出发向四个轴搜索,一旦找到连续六子立即终止。因为新棋子的出现才有可能产生胜利条件,其他位置不会因为这一手凭空赢棋。像这种小细节,每次看棋类项目源码时都能找到几处可以优化的地方,这就是项目实战的意义所在。
3. 界面层怎么做:QPainter绘制棋盘与坐标转换细节
很多初学Qt的朋友拿到棋类源码,最想搞明白的就是棋盘怎么画出来、棋子怎么放上去、点击位置怎么精确识别。这个项目用的是Widget + QPainter方案,而不是QGraphicsView框架。Widget + QPainter对棋类这种低交互复杂度的场景更轻量,绘制逻辑直白,一个paintEvent搞定所有,也便于后续把绘制逻辑搬到其他框架里。
3.1 棋盘绘制:从网格到星位
棋盘绘制的核心在BoardWidget::paintEvent()里。定义一个棋盘单元格边长,比如40像素,棋盘行数16列数16,那么整个棋盘的像素尺寸就是 (16-1)*40 = 600 像素。为什么是减1,因为棋盘线条数量是行列数减1,交叉点数量是行列数。如果棋盘类里还保留了一些边距padding,绘制区域还要额外留出边距。
绘制网格线的逻辑很直接:
void BoardWidget::paintEvent(QPaintEvent *) { QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing, true); painter.setPen(QPen(QColor("#5A4A3A"), 2)); for (int i = 0; i < boardSize; ++i) { painter.drawLine(margin + i * cellSize, margin, margin + i * cellSize, margin + (boardSize - 1) * cellSize); painter.drawLine(margin, margin + i * cellSize, margin + (boardSize - 1) * cellSize, margin + i * cellSize); } }棋盘线画完之后,再根据数据模型里的 board 数组,在对应交叉点绘制黑白棋子。棋子用渐变圆形来实现立体感,先画一个比棋子半径略大的阴影,再绘制径向渐变的圆,视觉上会漂亮很多。渐变看起来只是锦上添花,但项目截图和实际演示时的观感差异非常大。
3.2 点击识别:鼠标坐标到棋盘交叉点的换算
鼠标点击区域识别是棋类交互的命门。用户点下去的位置不可能正好落在交叉点上,需要把鼠标坐标换算成距离最近的交叉点行列号:
void BoardWidget::mousePressEvent(QMouseEvent *event) { int x = event->pos().x(); int y = event->pos().y(); int col = (x - margin + cellSize / 2) / cellSize; int row = (y - margin + cellSize / 2) / cellSize; if (row >= 0 && row < boardSize && col >= 0 && col < boardSize) { emit cellClicked(row, col); } }这里的关键是cellSize / 2这个偏移量。如果不加这个偏移,用户点到格子左上半区时会落到上一个交叉点,视觉上会觉得落子位置跟鼠标偏差很大。加上半格偏移后,相当于做了四舍五入,鼠标点击在哪个交叉点的势力范围内,就落到哪个点。这个细节源码里处理得非常标准,值得其他项目复用。
3.3 棋子高亮、最近一手标记与悔棋重绘
界面上至少还需要两个装饰:最近一手的棋子用不同的边框高亮,用于指示当前轮到哪一方下棋的状态提示。源码里用了一个简单的办法,在最近一手棋子的中心绘制一个小红点或小三角。逻辑上就是在 drawPiece 函数里多判断一次 row/col 是否等于 lastMove,是就额外画标记。
悔棋的实现相对直接:在落子之前把旧棋盘状态保存到一个操作栈中(比如QVector存储每一步之前的完整棋盘二维数组),悔棋时弹出栈顶恢复即可。同时记得把当前回合玩家切回上一步的状态。如果你连落子时间也算进战绩里,那还需要把时间点也入栈,一起恢复。网上不少版本的悔棋功能都有bug,重绘时机没处理好,恢复棋盘后不调用update(),导致界面没刷新,看起来就像没悔棋一样,实际数据已经变了。这一点实现的时候多留个心眼。
4. AI对手是怎么思考的:基于评分表的落子策略与线程隔离
人机对战是整个源码包里的技术高点。六子棋AI不用做到AlphaGo那种深度,教学项目里最常见也最有效的方案是基于评分表的贪心策略。这个策略的核心思路是把每一个空位打一个分数,分数由该点对攻防双方的棋型贡献决定,然后选最高分的位置落子。
4.1 棋型分数如何定义
评分表法的第一步是定义各种棋型的分数。棋型指在某个方向上,以某个空位为中心或端点,能连成的连续同色棋子形态。比如一个方向上已有4颗同色棋且两端都是空的,叫活四,几乎必胜;如果一端被堵住只有一端开放,叫冲四,也很危险。
这份源码里我看到的评分表类似这样(数值是我拆解后归纳的常见权重,具体值可能略有差异):
| 棋型 | 进攻分 | 说明 |
|---|---|---|
| 六连 | 1000000 | 已成六子,直接胜利 |
| 活五 | 100000 | 五子连珠且两端开放,一手成六 |
| 活四 | 50000 | 四子两端开放,几乎锁胜 |
| 冲四 | 8000 | 四子一端被堵,只能一个方向延伸 |
| 活三 | 3000 | 三子两端开放,可演变成活四 |
| 眠三 | 600 | 三子一端被堵,威胁降低 |
| 活二 | 300 | 两子两端开放 |
| 眠二 | 80 | 两子一端被堵 |
AI下棋时,对每个空位分别计算黑子和白子的总分。计算方法是:以该空位为轴心,向四个方向各统计与它相邻的同色连续棋子数以及两端的状态。然后根据棋型查表得分数,四个方向求和。
4.2 进攻分与防守分的权衡
当轮到AI下棋时,AI需要同时考虑攻击和防守:它要在自己的回合尽量形成攻势,同时阻止玩家形成必杀棋型。源码里常用的做法是对每个空位计算:
int score = attackScore(board, row, col, AI_PLAYER) + DEFENSE_WEIGHT * attackScore(board, row, col, HUMAN_PLAYER);其中DEFENSE_WEIGHT的取值影响AI的激进程度。如果取1.0,AI是完全理性的攻守平衡;取大于1.0,AI偏保守,优先堵玩家;取小于1.0,AI偏激进,优先自己进攻。我看到不少教学源码这里直接取1.0,也就是攻击和防守权重等同。但实际对局中发现,当双方都有潜在棋型时,AI应该优先处理威胁度更高的那个方向,而不是机械地相加。更好的做法是给冲四、活三这类致命威胁更高的防守权重,比如1.2甚至1.5。
4.3 AI计算放子线程,避免主界面卡死
如果AI落子用同步方式执行,棋盘一旦到了中盘后期,空位变多、评分计算量增大,界面就会卡顿,甚至出现未响应状态。实战项目中,正确做法是把AI计算放到子线程,计算完成后通过信号把落子坐标传回主线程。
class AIWorker : public QObject { Q_OBJECT public slots: void computeMove(const QVector<QVector<int>>& board, int aiPlayer); signals: void moveComputed(int row, int col); };在主线程创建QThread,把AIWorker对象moveToThread,连接界面“轮到AI下棋”的信号到AIWorker的computeMove槽,AI计算完发出moveComputed信号,主线程收到后更新棋盘。这样UI和AI计算互不干扰,界面动画也能保持流畅。
4.4 让AI有不同难度:搜索深度与随机噪声
基础评分表AI会被老手轻松击败,因为它的视线只有一步。要提升AI实力,一个很简单但效果明显的方法是引入两层搜索:先算出最高分的位置,然后再模拟在该位置落子后对手的最佳应对,选择一个对手应手后AI仍能保持高分的走法。这个思路本质上是MiniMax的极简版,虽然只往后看两层,但棋力会比纯贪心高出一个档次。
如果你想更贴近真实AI的优化空间,还可以加一个随机因子:分数差在5%以内的候选点随机选择,避免AI每局走法完全一致,对新手体验更友好。这部分源码如果再自己扩展,可玩性会提升很多。
5. 从源码到成品:编译发布与几个值得再优化的方向
项目源码跑通只是第一步,真正能拿来展示的作品还需要考虑打包发布问题,以及功能完整度层面的打磨。
5.1 打包发布:windeployqt与依赖排查
Qt程序在开发机上运行正常,复制到别的电脑双击没反应或报“无法定位程序输入点”,这是Windows平台最常见的发布问题。核心原因是Qt程序依赖一堆Qt运行库,这些库在开发环境里通过 PATH 环境变量能找到,到了目标机器上就失效了。
打包发布的标准姿势是:在Release模式下重新编译,然后用Qt自带的 windeployqt 工具把依赖打进去。命令行方式如下:
cd build目录/Release windeployqt SixInARow.exe运行后,工具会把Qt相关的dll、插件目录(platforms、styles等)、qss资源文件自动拷贝到exe同目录。经过这一步,整个文件夹就能拷给别人运行了。不过有几点经验想特别提醒:
- 如果程序用了编译器的运行库(比如libgcc_s_seh-1.dll、libstdc++-6.dll),这些不会由windeployqt自动处理,需要手动添加或者用静态编译方式彻底规避。
- 如果目标机器是Windows 7,而你在新版Windows上编译,可能还需要带上对应版本的UCRT库,这一步非常容易遗漏。
- Windows上经常遇到“no Qt platform plugin could be initialized”错误,就是platforms目录下少了qwindows.dll。
5.2 想要更完善?可以从这几个方向继续改
源码包能完整跑起来、能保存战绩、能人机对战,已经算一个不错的成果。但如果真想拿它当毕设、当面试项目,或者当成深入学习Qt的练手素材,以下几个点值得继续动手术:
- 增加局域网双人对战:用QTcpSocket传递坐标协议,玩家A落子后把行列号发给玩家B,双方保持同一个棋盘状态。这个功能一旦加上,项目复杂度立刻上一个台阶,也更有说服力。
- 增加游戏记录的序列化与复盘功能:把每一步落子保存成文件(JSON或自定义文本格式),以后可以回放整局。实现起来不算复杂,涉及QFile和QJsonDocument,非常锻炼Qt文件处理的能力。
- 加入禁手规则判定:真正的连珠玩法会有禁手限制,比如黑棋不能下双三或双四。加了禁手之后,胜利判定的复杂度会明显提升,AI的评分表也要跟着调整,是挑战性很高的扩展方向。
- 界面美化:QSS样式表统一换肤,加个指示当前操作状态的文字标签,落子时加个淡入动画。视觉效果是项目展示时最容易抓眼球的部分,值得投入时间。
功能扩展不用贪多,选一到两个方向做到位即可。说到底,一个源码包的价值不在于你下载下来能跑通,而在于你能从里面学会什么、改出什么。六子棋这个项目麻雀虽小五脏俱全,棋盘交互、AI评分、信号槽协作、Widget自绘,这些都是Qt开发绕不开的核心技能,把一份源码真吃透了,比泛泛地刷十个小demo都管用。
本文还有配套的精品资源,点击获取