尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
C++ Qt黑白棋项目实战:从零构建可交互桌面游戏
简介这是一份基于C与Qt框架实现的黑白棋翻转棋完整项目源码面向具备C基础、希望入门GUI应用开发或课程设计的编程学习者。项目围绕8x8棋盘展开涵盖棋盘二维数组建模、合法落子与翻转判定、游戏结束与计分逻辑、鼠标点击交互、界面信息展示及存档读档等核心模块并预留了基于最小-最大搜索配合Alpha-Beta剪枝或蒙特卡洛树搜索的AI扩展思路是理解数据结构、算法与Qt控件协同的实践范例。资源包共28个文件约1.12MB以cpp源文件与h头文件为主体辅以o目标文件、pro工程文件、ui界面文件及exe可执行程序另含Makefile与调试配置便于直接编译运行与二次修改。目前已有363人学习下载适合作为C与Qt图形界面开发的练手项目帮助读者掌握事件响应、棋局状态更新与序列化存储等关键技能。1. 黑白棋C Qt一个能直接跑起来的桌面棋类项目很多人学完 C 语法和 Qt 控件之后卡在“不知道拿什么练手”这一步。控件拖拽会了信号槽会连了但真让自己从零写一个完整可交互的桌面程序又不知道从哪下手。这个黑白棋项目就是冲着这个缺口来的它把棋盘绘制、落子规则、翻转判定、胜负结算、人机对战整合进一个 Qt Widgets 工程里代码量适中逻辑闭环完整拿来当第一个“能跑能改”的桌面项目正合适。它适合刚学完 Qt 基础、想找一个有真实交互逻辑的练手项目的开发者也适合想拿它当模板改造成其他棋类或回合制小游戏的人。下面按“它是什么、怎么跑、怎么改”的顺序拆一遍。2. 项目定位与技术选型为什么用 Qt Widgets 而不是 QML2.1 这个项目到底做了什么黑白棋Reversi / Othello的规则不复杂8×8 棋盘双方轮流落子落子后夹在己方两子之间的对方棋子全部翻转最后棋子多的一方获胜。规则简单但实现起来涉及几个关键点合法落子点的判定、八个方向的翻转检测、无子可下时的跳过处理、终局判定。这个项目把这些逻辑全部用 C 实现界面层用 Qt Widgets 承载棋盘用自绘方式渲染交互用鼠标点击驱动。从工程角度看它不是一个“玩具级”的 demo。棋盘状态、落子合法性、翻转计算、AI 决策这几块是分开的逻辑层和界面层没有完全耦合在一起。这意味着你可以单独测试规则逻辑也可以把 AI 换成别的策略而不动界面代码。对于想理解“一个桌面程序怎么分层”的人来说这个结构比那种所有代码堆在一个文件里的示例有价值得多。2.2 技术栈拆解项目用到的技术栈如下技术项用途说明C核心逻辑棋盘状态、规则判定、AI 搜索Qt Widgets界面框架主窗口、棋盘控件、按钮、状态栏QPainter棋盘绘制自绘棋盘格、棋子、提示标记QMouseEvent交互响应鼠标点击落子信号与槽模块通信界面与逻辑层的数据同步qmake / CMake构建生成 Makefile 或工程文件选 Qt Widgets 而不是 QML原因很直接这个项目的核心是逻辑不是动画和视觉效果。Widgets 的 QPainter 自绘足够画出清晰的棋盘和棋子信号槽机制处理交互也足够顺手。QML 更适合做流畅动画和复杂视觉但对一个棋类项目来说属于杀鸡用牛刀反而增加了学习成本。常见做法是逻辑密集、界面简单的桌面工具类程序优先用 Widgets。2.3 目录结构长什么样一个典型的 Qt Widgets 工程目录大致是这样组织的othello/ ├── othello.pro # qmake 工程文件 ├── main.cpp # 程序入口 ├── mainwindow.h/cpp # 主窗口 ├── boardwidget.h/cpp # 棋盘控件自绘 交互 ├── gamelogic.h/cpp # 游戏规则逻辑 ├── aiplayer.h/cpp # AI 决策 └── resources/ # 图标等资源这个划分的关键在于gamelogic不依赖任何 Qt 界面类它只处理棋盘数组和规则。boardwidget负责把逻辑层的棋盘状态画出来并把鼠标点击转换成坐标传给逻辑层。aiplayer接收当前棋盘状态返回一个落子位置。这种分层让每一块都能独立替换——比如你想把 AI 从简单贪心换成 Minimax只需要改aiplayer其他文件不动。注意如果你拿到的源码里逻辑和界面混在同一个类里建议先做一次拆分再改功能否则后面加功能会越改越乱。3. 核心模块拆解棋盘状态、翻转判定与 AI 决策3.1 棋盘数据结构与初始化棋盘用一个 8×8 的二维数组表示每个格子的值用枚举区分空、黑子、白子。初始化时在中心四个格子放上初始棋子这是黑白棋的标准开局。// gamelogic.h enum class Piece { Empty, Black, White }; class GameLogic { public: static const int SIZE 8; GameLogic(); Piece getPiece(int row, int col) const; bool isValidMove(int row, int col, Piece player) const; bool makeMove(int row, int col, Piece player); int countPieces(Piece player) const; bool hasValidMove(Piece player) const; Piece currentPlayer() const; void switchPlayer(); private: Piece board[SIZE][SIZE]; Piece current; void initBoard(); bool checkDirection(int row, int col, int dRow, int dCol, Piece player) const; void flipDirection(int row, int col, int dRow, int dCol, Piece player); };initBoard()把数组全部置为Empty然后在(3,3)、(4,4)放白子(3,4)、(4,3)放黑子。current初始为黑子黑方先手。这个结构的好处是棋盘状态完全由数组决定不依赖任何界面对象方便单独写测试。3.2 合法落子与八方向翻转判定黑白棋的核心规则是落子位置必须能夹住至少一个对方棋子。所谓“夹住”就是从落子点出发沿某个方向连续遇到对方棋子直到遇到己方棋子中间的所有对方棋子被翻转。八个方向都要检查。bool GameLogic::checkDirection(int row, int col, int dRow, int dCol, Piece player) const { int r row dRow; int c col dCol; int count 0; Piece opponent (player Piece::Black) ? Piece::White : Piece::Black; // 沿方向前进遇到对方棋子就继续 while (r 0 r SIZE c 0 c SIZE board[r][c] opponent) { r dRow; c dCol; count; } // 必须至少夹住一个对方棋子且终点是己方棋子 if (count 0 r 0 r SIZE c 0 c SIZE board[r][c] player) { return true; } return false; }isValidMove()遍历八个方向调用checkDirection()只要有一个方向返回 true这个位置就是合法的。makeMove()在确认合法后再次遍历八个方向调用flipDirection()执行翻转。这里有个容易翻车的地方checkDirection和flipDirection的方向数组必须一致否则会出现“判定合法但翻转不对”的玄学 bug。// 八个方向的偏移量 const int dirs[8][2] { {-1,-1},{-1,0},{-1,1}, {0,-1}, {0,1}, {1,-1}, {1,0}, {1,1} };3.3 无子可下与终局处理黑白棋有一个特殊规则如果当前玩家没有任何合法落子点必须跳过由对方继续下。如果双方都没有合法落子点游戏结束。这个逻辑如果漏掉程序会在某些局面下卡死。void GameLogic::switchPlayer() { current (current Piece::Black) ? Piece::White : Piece::Black; // 如果换人后对方无子可下再换回来 if (!hasValidMove(current)) { current (current Piece::Black) ? Piece::White : Piece::Black; } }hasValidMove()遍历整个棋盘对每个空格调用isValidMove()。如果双方都没有合法位置界面层应该弹出结算提示显示双方棋子数。常见做法是在makeMove()返回后检查hasValidMove()如果当前玩家无子可下且对方也无子可下就触发终局。3.4 AI 决策简单贪心与位置权重项目里的 AI 通常不会一上来就写 Minimax而是先用一个“贪心 位置权重”的策略遍历所有合法落子点对每个点评估一个分数选分数最高的。分数由两部分组成翻转棋子数量以及位置权重角位最高边位次之中心区域较低。// aiplayer.cpp QPoint AIPlayer::chooseMove(const GameLogic logic, Piece player) { QVectorQPoint moves getAllValidMoves(logic, player); int bestScore -1; QPoint bestMove moves.first(); for (const QPoint move : moves) { int score evaluateMove(logic, move, player); if (score bestScore) { bestScore score; bestMove move; } } return bestMove; } int AIPlayer::evaluateMove(const GameLogic logic, QPoint pos, Piece player) { // 位置权重表角位 100边位 20其他 1 static const int weight[8][8] { {100, -20, 10, 5, 5, 10, -20, 100}, {-20, -50, -2, -2, -2, -2, -50, -20}, { 10, -2, 1, 1, 1, 1, -2, 10}, { 5, -2, 1, 0, 0, 1, -2, 5}, { 5, -2, 1, 0, 0, 1, -2, 5}, { 10, -2, 1, 1, 1, 1, -2, 10}, {-20, -50, -2, -2, -2, -2, -50, -20}, {100, -20, 10, 5, 5, 10, -20, 100} }; int flipCount countFlips(logic, pos, player); return flipCount * 2 weight[pos.x()][pos.y()]; }这个权重表的逻辑是角位一旦占住就不会被翻转价值最高角位旁边的格子要扣分因为下在那里容易把角位让给对方。flipCount是模拟落子后能翻转的棋子数。这个 AI 不强但足够让单人模式有对抗感。想加强的话把chooseMove换成 Minimax 加 Alpha-Beta 剪枝即可接口不用变。4. 从零跑起来构建、编译与首次运行4.1 环境准备与依赖检查跑这个项目需要三样东西C 编译器、Qt 开发库、构建工具。Windows 上常见做法是装 Qt 官方在线安装器选一个 Qt 版本比如 Qt 5.15 或 Qt 6.x并勾选对应编译套件MinGW 或 MSVC。Linux 上用包管理器装qtbase5-dev和qmake即可。macOS 用 Homebrew 装qt。检查环境是否就绪qmake --version # 输出类似QMake version 3.1 / Using Qt version 5.15.2 g --version # 确认编译器可用如果qmake找不到说明 Qt 的 bin 目录没加进 PATH。Windows 上通常在Qt安装目录/版本/编译器/bin下Linux 上一般在/usr/lib/qt5/bin或/usr/bin。4.2 qmake 构建流程项目根目录下应该有.pro文件。构建流程是标准的 qmake 三步# 1. 生成 Makefile qmake othello.pro # 2. 编译 make # Linux/macOS mingw32-make # Windows MinGW # 或者用 nmakeMSVC # 3. 运行 ./othello # Linux/macOS othello.exe # Windows如果.pro文件里写了QT widgetsqmake 会自动链接 Widgets 模块。如果编译时报 “undefined reference to QApplication”检查.pro里有没有这行。常见做法是在.pro里加上CONFIG c11或c17避免枚举类语法报错。4.3 CMake 构建方式如果项目支持有些源码包用的是 CMake 而不是 qmake。这种情况下流程是mkdir build cd build cmake .. cmake --build . ./othelloCMakeLists.txt 里需要找到 Qt 的包find_package(Qt5 REQUIRED COMPONENTS Widgets) target_link_libraries(othello PRIVATE Qt5::Widgets)Qt 6 的话把Qt5换成Qt6。如果find_package失败需要设置CMAKE_PREFIX_PATH指向 Qt 安装目录。4.4 首次运行会看到什么程序启动后是一个主窗口中间是 8×8 棋盘初始四子摆好。窗口底部或侧边通常有状态栏显示当前轮到谁、双方棋子数。鼠标点击合法位置会落子并翻转非法位置点击无响应或给出提示。如果项目带 AI会有一个“人机对战”按钮或菜单项。如果带悔棋会有一个“撤销”按钮。提示第一次跑如果棋盘画出来了但点击没反应先检查boardWidget的mousePressEvent有没有正确计算格子坐标以及有没有调用update()触发重绘。5. 避坑与常见问题排查5.1 棋盘画出来了但点击落子位置偏移现象点击某个格子棋子落在旁边一格。原因通常是坐标换算时没有减去棋盘边距或者格子尺寸计算用了整数除法导致累积误差。解决在mousePressEvent里用(event-pos().x() - margin) / cellSize计算行列确保margin和绘制时的边距一致。如果棋盘居中还要考虑(width() - 8*cellSize) / 2的偏移。5.2 翻转方向不对或漏翻现象落子后某些方向的棋子没翻或者翻了不该翻的。原因多半是八个方向的偏移数组写错或者checkDirection和flipDirection用了不同的方向定义。解决把方向数组提取成全局常量两个函数共用。另外注意边界检查r 0 r SIZE不能漏。5.3 无子可下时程序卡死现象轮到某一方时没有任何合法落子点程序不跳过也不结束界面无响应。原因是switchPlayer里没有处理“对方也无子可下”的情况。解决在切换玩家后检查hasValidMove如果当前玩家无子可下再切一次如果双方都无子可下触发终局结算。5.4 编译报错 “QPainter not declared”现象编译时提示找不到 QPainter 或 QMouseEvent。原因是头文件没包含。解决在boardwidget.h里加上#include QPainter和#include QMouseEvent。Qt 6 里部分类的位置有调整如果用的是 Qt 6检查是否需要#include QtGui/QPainter。5.5 AI 走棋后界面不刷新现象AI 落子后棋盘没有更新但内部状态已经变了。原因是 AI 落子后没有调用update()触发重绘。解决在 AI 落子逻辑结束后调用boardWidget-update()或者通过信号槽通知界面刷新。常见做法是把 AI 落子放在QTimer::singleShot里延迟执行给界面刷新留出时间。6. 二次开发把黑白棋改成你自己的回合制游戏这个项目的结构决定了它很容易被改造成其他棋类或回合制游戏。核心思路是保留boardwidget的绘制和交互框架替换gamelogic的规则判定替换aiplayer的决策逻辑。下面以改成五子棋为例说几个关键改动点。第一步改棋盘数据结构和胜负判定。黑白棋的Piece枚举可以保留但isValidMove要改成“该位置为空即可落子”makeMove不再翻转而是检查五个方向是否有连续五子。胜负判定从“棋子数多者胜”改成“先连成五子者胜”。// 五子棋的胜负判定示例 bool checkWin(int row, int col, Piece player) { const int dirs[4][2] {{0,1},{1,0},{1,1},{1,-1}}; for (auto d : dirs) { int count 1; for (int step 1; step 5; step) { int r row d[0]*step, c col d[1]*step; if (r0||rSIZE||c0||cSIZE||board[r][c]!player) break; count; } for (int step 1; step 5; step) { int r row - d[0]*step, c col - d[1]*step; if (r0||rSIZE||c0||cSIZE||board[r][c]!player) break; count; } if (count 5) return true; } return false; }第二步改 AI 评估函数。黑白棋的位置权重表对五子棋不适用。五子棋的 AI 评估通常基于“连子数量”和“开放程度”活三、冲四、活四的分数依次递增。常见做法是维护一个模式匹配表对每个空位模拟落子后扫描四个方向统计己方和对方的连子形态。第三步改界面提示。黑白棋的状态栏显示“黑子 12 / 白子 8”五子棋改成“轮到黑方 / 白方”。棋盘绘制可以保留但不需要显示“可落子提示点”的话可以把相关绘制代码注释掉。第四步验证方法。改完之后先手动下几局确认胜负判定正确再让 AI 对 AI 跑一批对局检查有没有非法落子或死循环。我一般会写一个简单的批量测试随机生成合法落子序列跑一万局看有没有崩溃或异常状态。注意改规则时最容易翻车的地方是“胜负判定”和“合法落子判定”的边界条件。比如五子棋里如果落子后同时形成两个五连应该判胜如果棋盘下满还没人连成五子应该判平局。这些边界情况不处理测试时就会遇到玄学 bug。从那以后我每次改这类回合制游戏都会先把“合法落子 → 执行落子 → 胜负判定 → 切换玩家”这条链路单独抽出来写单元测试确认逻辑层没问题再接界面。这样即使界面改乱了核心规则也不会出错。希望帮到你。本文还有配套的精品资源点击获取
RELATED

相关推荐

Flutter与鸿蒙二进制编解码适配实战

Flutter与鸿蒙二进制编解码适配实战

1. 项目背景与核心价值在跨平台开发领域,Flutter因其高效的渲染性能和一致的UI体验已成为移动端开发的主流选择之一。而binary_codec作为Flutter生态中处理二进制数据编解码的关键底层库,其重要性不言而喻。随着鸿蒙系统的快速崛起,开发者面临…

📅 2026/10/15 12:56:27
Spring框架Bean管理:从基础概念到最佳实践

Spring框架Bean管理:从基础概念到最佳实践

1. Spring框架中的Bean基础概念在Spring框架中,Bean是最核心的概念之一。简单来说,Bean就是由Spring容器管理的Java对象。与传统的new关键字创建对象不同,Spring中的Bean是由IoC容器负责实例化、配置和组装的。我第一次接触Spring时&#xff…

📅 2026/10/15 12:56:27
前端入门必读:Web、网页与浏览器核心概念全解析

前端入门必读:Web、网页与浏览器核心概念全解析

1. 从零认识前端:Web、网页与浏览器到底是怎么回事很多人刚接触前端开发的时候,脑子里其实是一团浆糊。打开编辑器敲了几行代码,浏览器里跑出来一个页面,但你要问他“Web是什么”“网页是怎么变成你看到的样子的”“浏览器到底在背…

📅 2026/10/15 12:56:27
MORE NEWS

更多资讯

📰

SpringBoot生鲜电商平台架构设计与实现

1. 项目背景与核心价值农产品电商平台是近年来"互联网农业"领域的热门方向。作为一名长期关注农业数字化转型的开发者,我发现传统农副产品销售存在几个痛点:一是产销信息不对称导致农产品滞销,二是中间环节过多造成农民收益被压缩&…

📰

Flutter时间库chrono_dart鸿蒙适配与优化实践

1. 项目背景与核心价值在跨平台开发领域,时间处理一直是复杂业务场景中的关键痛点。chrono_dart作为Flutter生态中广受好评的时间序列处理库,为开发者提供了强大的时间轴管理、精密计算和时序资产控制能力。而随着鸿蒙系统的快速崛起,如何让这…

📰

二维码批量生成技术:原理、实现与优化实践

1. 二维码批量制作的价值与应用场景二维码已经成为现代商业和个人生活中不可或缺的一部分。从产品包装到活动海报,从支付系统到信息共享,这些黑白相间的小方块无处不在。但当你需要一次性生成几十甚至上百个二维码时,手动逐个创建显然效率低下…

📰

CentOS 7最小化系统部署MySQL 8.0实战指南

1. 项目概述:为什么要在CentOS 7最小版上“硬核”集成MySQL?在某高校实验室搭建一套用于Java Web教学演示的全栈开发环境时,我接手了一个看似常规、实则暗藏玄机的任务:在一台仅安装了CentOS 7 Minimal ISO的物理服务器上&#xf…

📰

context-mode:基于SQLite FTS5与MCP的上下文数据调度模式

1. 项目概述:什么是 context-mode?它不是玄学,而是可落地的数据上下文调度机制“context-mode”这个词最近在开发者社区里频繁出现,但它既不是某个新发布的框架,也不是某家大厂刚开源的黑科技库。它本质上是一种数据访…

📰

Codex最强模型来了:GPT5.6 Sol,来看看有什么不同吧|TaoToken 统一 Key 实测

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬