ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

MFC扫雷实现全解析:从消息映射到双缓冲绘制

MFC扫雷实现全解析:从消息映射到双缓冲绘制 简介基于MFC框架实现的经典扫雷游戏完整项目压缩包内含可运行的exe与Visual C工程源码既适合初学Windows编程的开发者阅读也可直接运行体验游戏。资源共54个文件压缩后2.96MB涵盖C头文件与实现文件、MFC工程配置、位图图标与音效资源以及调试生成物等目录结构完整。已有146人学习下载。透过源码可重点理解MFC的CWnd窗口管理、消息映射处理鼠标点击、CDC绘制游戏面板、地雷生成与翻开逻辑等核心机制同时提供DlgHero、DlgCustom、DlgNewRecord等对话框类展示了菜单交互与自定义游戏设置的做法。对想用C/MFC做小游戏练手的开发者而言这是一份可运行、可修改、可调试的入门参考资料。1. 经典MFC扫雷为什么说它是Windows GUI编程的入门教科书扫雷这种游戏看着简单真要自己动手用 MFC 实现一遍才算把 Windows 消息驱动机制摸透了。MFC 实现扫雷这个项目saolei.rar是一个完整的 VC6 工程主框架、棋盘绘制、自定义难度、排行榜全都有适合刚啃完 C 语法想入门 Windows GUI 的人也适合要快速复习 MFC 消息映射和 CDC 绘图的从业者。我始终觉得扫雷是所有 MFC 例子里性价比最高的一个界面不复杂但消息、绘制、资源、状态管理全都覆盖到了看懂这个工程再去做 MFC 状态栏显示、弹对话框这类日常需求就是顺水推舟的事。2. 先看家底saolei.rar 里的文件分别管什么拿到压缩包第一件事不是急着双击 exe而是把文件清单过一遍。VC6 时代一个完整 MFC 项目会生成十几个后缀奇怪的文件很多人一看到 .ncb、.clw、.opt 就懵了其实它们各司其职搞清楚之后再动手改代码会从容很多。2.1 工程文件那一堆后缀dsw、dsp、ncb 都是干嘛的VC6 的工程体系分两层最外层是工作空间.dsw里面装的是工程每个工程对应一个 .dsp 文件编译链接参数都写在里面。打开项目时双击 Mine.dsw 即可VC6 会按 .dsp 里的配置加载源码。如果现在用的 IDE 是 VS 2008 以上版本老 VC6 工程通常建议用“从现有代码创建新项目”的方式迁移直接双击 .dsw 在新版 VS 里有兼容问题。辅助文件里.ncb 是类浏览器数据库.clw 是 ClassWizard 的辅助索引这两个属于“可再生文件”——删掉后用 IDE 重新打开会自动生成。.opt 保存的是工作区窗口布局和断点.plg 是编译日志不影响程序本身。常见做法是老工程拷到新机器上第一次打开时如果 IDE 弹出各种后缀格式不兼容的警告直接把 .ncb、.clw、.opt 全部删掉再重新打开干净利落。2.2 源码文件与类模块的对应关系项目正文里的文件清单可以分成三组工程壳文件、业务源码、资源文件。把它们对应到 MFC 程序结构里就一目了然了。文件模块角色职责说明Mine.h / Mine.cpp主对话框类游戏主窗口、菜单交互、整体状态管理MineWnd.h / MineWnd.cpp棋盘窗口类从 CWnd 派生负责格子绘制与鼠标点击MineDefs.h公共定义区常量、枚举、面板尺寸等全局配置DlgCustom.h / DlgCustom.cpp自定义难度对话框输入行数、列数、雷数DlgNewRecord.h / DlgNewRecord.cpp新纪录对话框胜利后录入玩家名字DlgHero.h / DlgHero.cpp英雄榜对话框展示历史最佳成绩Mine.rc / resource.h资源文件菜单、图标、对话框模板、字符串StdAfx.h / StdAfx.cpp预编译头集中引入 MFC 公共头文件加速编译这个结构是典型的三层拆分对话框负责“壳”棋盘窗口负责“画”Mine.cpp 负责“算”。我拆过不少 MFC 老项目扫雷这个分层算是很清晰的——界面和逻辑分离改难度、改界面样式都不需要重写算法。Debug 目录是编译输出目录里面有完整可执行文件但我会建议你拿到源码后自己编译一次比直接跑 exe 更能发现问题。2.3 MineDefs.h整个游戏的公共定义区MineDefs.h 是整个工程的“黑匣子”入口。扫雷的面板尺寸、雷数上限、格子状态都会集中定义在一个公共头文件里方便所有类引用。常见的定义方式是这样// MineDefs.h游戏公共常量定义常见实现 enum GameLevel { LEVEL_BEGINNER 0, // 初级9 x 910 颗雷 LEVEL_INTERMEDIATE, // 中级16 x 1640 颗雷 LEVEL_ADVANCED, // 高级16 x 3099 颗雷 LEVEL_CUSTOM // 自定义难度 }; #define ROW_MAX 30 // 行数上限防止数组越界 #define COL_MAX 30 // 列数上限 #define MINE_MAX 99 // 雷数上限从这套定义能看出作者在设计时就已经考虑到自定义难度边界行列上限卡在 30高级模式 16×30 正好落在边界内。雷数上限 99 也与高级难度对齐避免自定义时雷数超过格子总数导致布局无解。这些边界参数是扫雷工程最容易翻车的地方稍后避坑章节会专门展开。3. 核心算法拆开看布雷、翻开与洪水扩散的完整实现扫雷的乐趣在于逻辑推理但代码层面的核心其实只有四件事存储面板、布雷、翻开格子、判断胜负。这一章我把每一项展开代码是这类项目最常见的实现方式拿到工程后可以对照源码找对应函数。3.1 面板数据结构二维数组加状态枚举扫雷面板本质就是一个二维矩阵。每一格有两个关键属性一个是格子里是什么数字还是雷一个是格子当前处于什么交互状态未翻开、已翻开、插旗、问号。所以常见的做法是用两个同样大小的二维数组一个存雷和数字一个存状态。// MineDefs.h 中典型的格子状态定义常见实现 enum CellState { CELL_HIDDEN 0, // 未翻开 CELL_OPEN, // 已翻开 CELL_FLAG, // 玩家插旗标记 CELL_QUESTION // 问号标记再点一次取消 }; // 面板数据 #define CELL_MINE -1 // 用 -1 表示该格是雷逻辑说明格子数值用 -1 表示雷08 表示周围雷数与状态枚举分开存储。为什么分开因为绘制时状态决定画旗子还是画数字数值决定数字大小判定时看数值是否为 -1计算胜负时看状态是否为 CELL_OPEN。合在一起反而麻烦。参数说明两套数组的索引保持一致访问时用m_board[row][col]和m_state[row][col]同步读写避免错位。3.2 布雷逻辑随机数和首点保护布雷有两个硬性要求雷数必须精准位置必须随机。最直白的做法是循环随机取坐标如果该位置还不是雷就放一颗直到放满指定数量。传统扫雷还有一条隐形规则——第一次点击不能踩雷否则游戏体验极差。我一般会在初始化布雷时把首点的周围 9 格全部排除在雷区外。// 布雷实现按难度在面板上随机放置指定数量的雷 void CMineWnd::InitMines(int rows, int cols, int mineCount, int safeRow, int safeCol) { int placed 0; srand((unsigned)time(NULL)); while (placed mineCount) { int r rand() % rows; int c rand() % cols; // 首点保护第一次点击点及其周围 8 格不布雷 if (r safeRow c safeCol) continue; if (abs(r - safeRow) 1 abs(c - safeCol) 1) continue; if (m_board[r][c] ! CELL_MINE) { m_board[r][c] CELL_MINE; placed; } } // 计算每个非雷格周围的雷数 for (int i 0; i rows; i) { for (int j 0; j cols; j) { if (m_board[i][j] ! CELL_MINE) { m_board[i][j] CountAdjacentMines(i, j); } } } }逻辑说明safeRow和safeCol是第一次点击的坐标跳过这格和它周围 8 格能保证开局必开一片。循环里continue跳到下一次随机直到布雷数量达标。最后的双重循环把每个非雷格周围的雷数统计出来填到m_board[i][j]里这样后面绘制时直接读数字即可不需要每次点击都重算周围雷数。参数说明自定义模式雷数太多、格子太少时首点保护可能让while循环永远凑不满雷数所以自定义难度的雷数必须做上限校验这是很多教学工程忽略的边界。3.3 翻开格子的洪水填充算法扫雷里最爽的体验是点开一个大格后瞬间展开一片空白区域这个效果由洪水填充Flood Fill算法实现。原理很简单当你翻开的格子周围雷数为 0就自动把周围 8 格也翻开如果翻开的格子又是 0就继续扩散直到碰到带数字的格子停下来。递归写起来最直观但遇到大面板没控制好深度会栈溢出所以我这里用队列做广度优先搜索// 洪水填充翻开格子并扩散到相邻的空格 void CMineWnd::FloodOpen(int row, int col) { std::queuestd::pairint, int q; q.push(std::make_pair(row, col)); while (!q.empty()) { int r q.front().first; int c q.front().second; q.pop(); if (r 0 || r m_rows || c 0 || c m_cols) continue; if (m_state[r][c] CELL_OPEN) continue; m_state[r][c] CELL_OPEN; m_openCount; if (m_board[r][c] 0) { for (int dr -1; dr 1; dr) { for (int dc -1; dc 1; dc) { if (dr ! 0 || dc ! 0) { q.push(std::make_pair(r dr, c dc)); } } } } } }逻辑说明出队一个格子后先做边界检查和状态检查已翻开的格子直接跳过防止重复入队造成死循环。当格子数值为 0 时把周围 8 格全部入队数值非 0 时只翻开自己就停止扩散——数字格子就是边界。m_openCount每次翻开都自增为胜负判定提供依据。参数说明队列用utility里的std::pair存坐标如果你编译的老环境不支持 C11可以换成自定义结构体或两个队列分别存行列。这个算法的时间复杂度是 O(n)最坏情况是整张图 480 格全部入队一次对 Windows 消息循环来说完全无压力。3.4 胜负判定与剩余雷数修正胜负判定在每次翻开动作之后执行。胜利条件很明确非雷格全部被翻开等价于m_openCount等于总格子数减雷数。失败条件更简单翻开时发现m_board[r][c] CELL_MINE。传统扫雷还有一个贴心细节——当玩家插旗标记了某格即使那格不是雷也要用插旗数去反推剩余雷数让界面状态始终反映真实情况。// 翻开操作入口判断是否踩雷否则走洪水填充并检查胜利 void CMineWnd::OnLeftClick(int row, int col) { if (m_gameOver || m_state[row][col] CELL_OPEN) return; if (m_board[row][col] CELL_MINE) { // 踩雷翻开所有雷游戏结束 RevealAllMines(); m_gameOver true; return; } FloodOpen(row, col); // 胜利条件翻开的非雷格数达到全场非雷总数 if (m_openCount m_rows * m_cols - m_mineCount) { m_gameOver true; PlayWinSound(); } }逻辑说明入口处先拦截游戏已结束状态和重复点击保证逻辑不会被连续鼠标事件搞乱。踩雷后调用RevealAllMines()把所有雷亮出来这是给玩家的“死后复盘”机会。胜利判断放在洪水填充之后因为一次展开可能一口气翻开几十格m_openCount可能直接从胜利前跳到胜利后所以必须在每次翻开后重新判定。参数说明m_mineCount是当前难度雷数初级 10、中级 40、高级 99自定义则是用户输入值判定公式把雷数排除在外保证雷数变化不会影响胜利阈值。4. 界面与交互消息映射、CDC 绘制和双缓冲那一套算法解决“游戏能不能跑”界面解决“玩家能不能玩”。MFC 的界面代码集中在消息映射和绘图上这也是很多人觉得 MFC 难上手的地方。其实拆开看就两类问题怎么让鼠标点击进入函数怎么把面板状态画到屏幕上。4.1 消息映射鼠标事件怎么进到游戏逻辑MFC 消息映射宏把 Windows 消息与类成员函数绑定。扫雷涉及的核心消息有左键点击翻开、右键点击插旗、双击翻开周围、菜单命令切换难度、定时器更新计时。看工程里的消息映射表大致长这样// 消息映射表把鼠标和菜单事件绑定到处理函数 BEGIN_MESSAGE_MAP(CMineWnd, CWnd) ON_WM_LBUTTONDOWN() ON_WM_RBUTTONDOWN() ON_WM_LBUTTONDBLCLK() ON_WM_TIMER() ON_COMMAND(ID_GAME_BEGINNER, CMineWnd::OnGameBeginner) ON_COMMAND(ID_GAME_INTERMEDIATE, CMineWnd::OnGameIntermediate) ON_COMMAND(ID_GAME_ADVANCED, CMineWnd::OnGameAdvanced) ON_COMMAND(ID_GAME_CUSTOM, CMineWnd::OnGameCustom) END_MESSAGE_MAP()逻辑说明ON_WM_LBUTTONDOWN()是把 WM_LBUTTONDOWN 消息映射到类的OnLButtonDown成员函数上MFC 框架在窗口收到鼠标消息时自动查表调用对应函数。ON_COMMAND则处理菜单项的 0x111 命令消息把“菜单点击”和“难度切换”绑定。参数说明消息映射表的顺序有讲究同一消息只能映射一次如果两个处理函数都想响应同一消息需要自己在函数里做二次分发而不是在映射表里重复写。这对有编程基础的人来说可能觉得绕但 MFC 就这么设计习惯了反而觉得很直观。左右键同时按下的经典操作逻辑上走的是另一条路在左键点击函数里判断GetKeyState(VK_RBUTTON)是否小于 0如果是就先放开普通翻开逻辑改为翻开周围 8 格。这个操作官方叫 Chord新手往往忽略但它是扫雷进阶操作的核心源码里值得仔细看这部分处理。4.2 格子绘制CDC 画矩形和数字的颜色规则MFC 绘图靠的是 CDC 类。每个格子本质是一个矩形绘制逻辑分两态未翻开时画凸起边框已翻开时画平底色数字用经典颜色规则显示。传统扫雷数字颜色是玩家肌肉记忆的一部分1 蓝色、2 绿色、3 红色、4 深蓝、5 深红、6 青色、7 黑色、8 灰色。颜色查找表一般是独立的static数组。// 绘制单个格子按状态画凸起、按下或旗帜 void CMineWnd::DrawCell(CDC* pDC, int row, int col) { CRect rc GetCellRect(row, col); if (m_state[row][col] CELL_OPEN) { pDC-FillSolidRect(rc, RGB(220, 220, 220)); // 已翻开底色 if (m_board[row][col] 1) { CString str; str.Format(_T(%d), m_board[row][col]); pDC-SetTextColor(GetNumberColor(m_board[row][col])); pDC-SetBkMode(TRANSPARENT); // 背景透明否则盖住底色 pDC-DrawText(str, rc, DT_CENTER | DT_VCENTER | DT_SINGLELINE); } } else { pDC-FillSolidRect(rc, RGB(180, 180, 180)); // 未翻开底色 if (m_state[row][col] CELL_FLAG) { // 简化旗子画法一个红色小矩形 黑色斜线 pDC-FillSolidRect(rc.left 4, rc.top 4, rc.Width() - 8, rc.Height() - 8, RGB(255, 0, 0)); } } }逻辑说明GetCellRect把行列坐标换算成像素矩形客户区尺寸变化时只要在OnSize里更新格子宽高绘制函数就不用改。数字绘制有两点容易翻车一是忘了SetBkMode(TRANSPARENT)文字背景会盖掉格子底色二是SetTextColor只改变文字颜色数字颜色规则必须自己维护。参数说明DrawCell被逐格调用绘制效率取决于被调用的次数整盘重绘时 480 格逐格 DrawText性能压力不大但如果每一格都重新创建画笔和字体就会明显卡顿所以画笔和字体最好在OnCreate里一次性创建好绘制时直接SelectObject。4.3 双缓冲绘制内存 DC 消除闪烁直接在一张可见窗口 DC 上逐格画矩形和文字会导致边界闪烁这是很多 MFC 游戏的通病。因为每画一格都直接写显存频繁重绘时用户能看到明显的“闪白”过程。标准解法是双缓冲先在内存里画完整张面板然后一次性把整幅图像拷到屏幕。// 双缓冲 OnPaint先画到内存 DC再整体 BitBlt 到窗口 void CMineWnd::OnPaint() { CPaintDC dc(this); // 窗口 DC CDC memDC; // 内存 DC memDC.CreateCompatibleDC(dc); CBitmap bmp; bmp.CreateCompatibleBitmap(dc, m_clientWidth, m_clientHeight); CBitmap* pOld memDC.SelectObject(bmp); for (int i 0; i m_rows; i) { for (int j 0; j m_cols; j) { DrawCell(memDC, i, j); // 全部画到内存 } } dc.BitBlt(0, 0, m_clientWidth, m_clientHeight, memDC, 0, 0, SRCCOPY); memDC.SelectObject(pOld); // 恢复原对象释放前解锁 }逻辑说明CreateCompatibleDC创建一块与窗口兼容的内存画布但刚创建时它自带的是一个 1×1 单色位图只有CreateCompatibleBitmap创建同尺寸真彩位图并SelectObject选进去之后内存 DC 才是真正的画布。如果不做这一步画上去的内容尺寸不对、颜色异常这是 MFC 新手最容易遇到的“画出来全是黑的/花的”的玄学问题。最后BitBlt把整幅图像一次性拷贝到窗口整个过程屏幕只刷新一次闪烁自然消失。参数说明SRCCOPY是拷贝方式意思是直接覆盖目标区域不透色不透明处理。注意窗口尺寸变化时m_clientWidth/m_clientHeight要跟着更新否则 BitBlt 区域和窗口实际尺寸不匹配会出现拖影。4.4 对话框参数传递自定义难度和排行榜扫雷的自定义难度对话框 DlgCustom、胜利后的新纪录对话框 DlgNewRecord、高手榜 DlgHero走的都是同一条 MFC 交互路径DoModal模式对话框 控件值读取。经常能在论坛看到有人问 MFC 状态栏怎么显示、在现有 VS MFC 工程上增加按钮弹出对话框并显示实时数据图表其实核心就是这套——一个对话框模板配一个 CDialog 派生类调用DoModal弹出来结束后从控件上取数据。// 自定义难度弹出对话框并读取玩家输入常见实现 void CMineWnd::OnGameCustom() { DlgCustom dlg; dlg.m_rows m_rows; // 传入当前值方便玩家在旧值上修改 dlg.m_cols m_cols; dlg.m_mines m_mineCount; if (dlg.DoModal() IDOK) { m_rows dlg.m_rows; m_cols dlg.m_cols; m_mineCount dlg.m_mines; InitGame(); } }逻辑说明DlgCustom 里声明三个成员变量m_rows、m_cols、m_mines并给对应编辑框控件绑定 DDX 变量DoModal返回后 MFC 已经自动把编辑框里的字符串转成整型填回成员变量。调用方只需要判断返回值是IDOK还是IDCANCEL来决定是否应用配置。参数说明向对话框传当前值作为初值是常见的友好交互做法否则每回打开都是空白的玩家容易改错。InitGame()负责按新模式重新布雷、重置状态和计时自定义模式下行列和雷数都变了之前的面板数组要么动态分配、要么按上限 30×30 静态声明后者更省事——这也解释了为什么 MineDefs.h 里会有ROW_MAX/COL_MAX两个上限常量。5. MFC扫雷避坑指南编译、重绘与资源文件的常见问题这章全是血泪经验。扫雷工程本身不大但想在现在的主流 Windows 版本上把老 VC6 工程编译跑起来会遇到一批时代留下的坑。我把最常见的问题按“现象 → 原因 → 解决”列出来每一条都值得在你自己的复现流程里提前规避。5.1 现象双击 Mine.dsw 编译报错提示找不到 afxwin.h 或无法打开 MFC 类库原因VC6 的默认安装路径里 MFC 目录不在系统 Include 搜索链上或者你的机器装的是 VS 高版本而不是 VC6MFC 类库目录结构不一样。另一个常见情况是把工程拷到新电脑后VC6 的安装路径变了但工程里保存的绝对路径还指向旧位置。解决先在 VC6 的 Tools → Options → Directories 里确认 Include 和 Lib 分别指向...\VC98\MFC\INCLUDE、...\VC98\MFC\LIB如果是用新版 VS 打开老工程不要直接转项目用“从现有代码创建新项目”重新组织工程再把 .cpp/.h 全部加入进来编译通过率高得多。5.2 现象ClassWizard 打开后看不到任何类或者一打开就崩溃原因.clw 和 .ncb 是旧编译器生成的索引文件工程被移动、重命名或增删过源码后索引和实际文件对不上ClassWizard 读索引时直接翻车。这个文件对程序本身没有影响但 IDE 功能会废掉一半。解决把工程目录下的 .clw、.ncb、.opt 全部删干净重新打开 .dswIDE 会自动重建索引。如果重建后 ClassWizard 还是空的检查所有头文件是否都还在工程目录里特别是 StdAfx.h 有没有被误删或改名。5.3 现象点击格子时整个面板闪烁或者地图边缘出现残影原因没有双缓冲或者重绘时用了Invalidate()整窗刷新。另外如果只在OnLButtonDown里调了InvalidateRect(GetCellRect(row, col))但没调UpdateWindow()WM_PAINT 消息会在消息队列里排队延后处理玩家看到的界面滞后于点击。解决OnPaint 里走完整双缓冲流程见 4.3局部刷新时用InvalidateRect指定目标区域后再UpdateWindow()强制立即重绘这样点击的格子会马上响应不至于出现“点了没反应、等其他消息处理完才一起刷”的滞涩感。5.4 现象左右键同时按下时周围格子没有一起翻开原因扫雷的 Chord 操作需要同时检测左右键状态但默认情况下 MFC 的OnLButtonDown只能拿到左键消息右键按下前、松开后的事件在时间上不保证与左键重叠。如果你只在WM_LBUTTONDOWN里处理逻辑自然感知不到右键当前是否被按住。解决在左键处理函数开头用GetKeyState(VK_RBUTTON) 0判断右键是否处于按下状态如果按下了就进入 Chord 分支翻开当前格子周围所有已插旗格对应的相邻区域同时要处理右键单独点击的插旗逻辑两者路径分离避免重复触发。调试时可以先按住右键不放再点左键观察输出是否符合预期。5.5 现象程序编译通过但运行时报缺少 MFC42D.dll 或资源加载失败游戏界面一片空白原因Debug 版本默认动态链接 MFC 库调试运行时依赖 VCDebug 环境里的 MFC42D.dll把可执行文件单独拷走就缺库资源加载失败则是 res 文件夹没有和 Mine.rc 放在同一目录下或者资源文件被单独拷走后没有连带复制。解决想分发单文件就把工程配置改成静态链接 MFCProject → Settings → General → Microsoft Foundation Classes 选择 Use MFC in a Static Library重新编译 Release资源问题则检查 Mine.rc 里的#include res\\xxx.ico路径确认 res 目录和 .rc 文件相对位置正确编译后Mine.exe所在目录要能看到对应的位图和图标文件。6. 进阶改造计时器、难度扩展与固定雷阵验证法扫雷的计时逻辑很多人以为是读系统时间其实 MFC 工程里最常见的是SetTimer加消息映射。在游戏初始化后调用SetTimer(1, 1000, NULL)每 1000 毫秒触发一次WM_TIMER消息在OnTimer里递增秒数并更新标题栏或状态栏。游戏结束或重新开始时用KillTimer(1)清掉定时器避免消息堆积。用状态栏显示剩余时间时就是经常被问到的“MFC 状态栏怎么显示”场景——拿着m_wndStatusBar.SetPaneText直接刷新即可扫雷正好是一个天然的时间刷新状态栏的例子。// 计时器初始化与消息处理每秒更新一次计时显示 void CMineWnd::StartTimer() { SetTimer(1, 1000, NULL); // 1 秒触发一次NULL 表示走消息映射默认处理 m_elapsed 0; } void CMineWnd::OnTimer(UINT_PTR nIDEvent) { if (nIDEvent 1 !m_gameOver) { m_elapsed; CString text; text.Format(_T(时间%d 秒), m_elapsed); SetWindowText(text); // 也可以改用状态栏显示 } CWnd::OnTimer(nIDEvent); }难度扩展是扫雷另一个很自然的改造点。经典三档是初级 9×9×10、中级 16×16×40、高级 16×30×99但自定义模式理论上能覆盖 9×9 到 30×30 的任意尺寸。改难度最稳妥的方式是先在 MineDefs.h 里把三个难度枚举和默认参数写好再保证InitGame里所有数组访问都走m_rows/m_cols而不是写死数字。很多老工程喜欢在绘制时写死格子尺寸这会导致切换难度后棋盘绘制错位改成动态计算格子宽高是必须的动作。验证扫雷逻辑最有效的方法不是狂点鼠标碰运气而是用固定雷阵做回归测试。把随机布雷换成手工指定的雷阵让每局开局雷的位置完全一样这样才能验证特定布局下的翻开、插旗、Chord 和胜负判定是否全部正确// 固定雷阵验证替换随机布雷手工指定雷的位置 int testMines[][2] { {0, 0}, {0, 1}, {3, 3}, {3, 4}, {7, 15} }; void CMineWnd::InitFixedMines() { memset(m_board, 0, sizeof(m_board)); for (int i 0; i 5; i) { m_board[testMines[i][0]][testMines[i][1]] CELL_MINE; } for (int i 0; i m_rows; i) { for (int j 0; j m_cols; j) { if (m_board[i][j] ! CELL_MINE) { m_board[i][j] CountAdjacentMines(i, j); } } } }固定雷阵的好处是每次改动代码后你都知道哪一格翻开应该出现什么数字可以准确判断逻辑是否回归。断言失败时一目了然不用靠“手感”猜问题在哪。测试顺序我习惯这样走先单格翻开验证数字、再验证 0 格洪水填充展开范围、接着验证插旗后 Chord 行为、最后验证胜负判定的边界条件一套走完再切回随机布雷做随机性冒烟测试。从那以后我每次动扫雷的代码都强制先跑一遍固定雷阵再谈其他功能这个习惯救过我很多次翻车现场。希望帮到你。本文还有配套的精品资源点击获取
返回列表