☰
数据结构与算法实验:连连看游戏设计与消子算法全解析
2026/10/3 1:22:52 网站建设 项目流程

简介:来自武汉理工大学的《数据结构与算法综合实验连连看》实验报告,面向学习数据结构、C++与MFC的在校生和课程设计人员。报告以“欢乐连连看”为项目载体,完整梳理了16×10游戏地图的二维数组存储、消子判断等核心算法,涵盖一条直线、两条直线、三条直线连通判断,以及胜负判定、提示、重排、计时和三种游戏模式的设计实现,并配有关键代码与异常处理说明,可直接用于实验参考或答辩准备。资源为单个docx文档,大小约1.37MB,内容包含实验目的、分析设计、核心算法流程与实现代码等模块。已有227人学习浏览,适合需要完成同类综合实验、理解数组与栈应用及MFC/GDI图形界面开发的读者。

1. 把实验报告变成可复现的工程:一份数据结构与算法综合实验连击

这份“武汉理工大学数据结构与算法综合实验连连看.docx”并不是一个可以直接双击运行的游戏安装包,而是一份完整的、可以照着敲出来的课程设计报告——它的价值在于把“连连看”这个看似简单的游戏,拆解成了数据结构课程里最经典的几个考点:二维数组存储、栈保存路径、三条直线连通判断。我在拿到这份文档后第一反应是打开看里面有没有完整代码,结果发现代码片段、类设计、调试记录都在,缺的是把它们串起来跑通的工程配置步骤。如果你正在做同类课程设计,或者想把“消子算法”这种经典问题练熟,这份文档能帮你省下大量理清思路的时间。本文就把这份报告里的核心设计、代码逻辑、踩坑点全部拆出来讲透,方便你直接复现和改写。

2. 游戏地图与数据结构设计:二维数组、顶点结构体与地图生成规则

2.1 从地图像素到逻辑坐标:为什么用二维数组而不是链表

连连看的地图是 16 行乘 10 列的矩形,共 160 个小方格,每格放一张 40×40 的图片。从数据结构角度看,这是典型的“位置即索引”场景——用户点击屏幕上的某张图片,实际上点击的是某个逻辑坐标 (row, col),需要在常数时间内拿到该位置的图片编号。链表做不到这一点,因为要拿到第 i 行第 j 列的元素需要遍历。所以报告里选择使用 int 类型的动态二维数组 int **m_pGameMap 来存储地图数据,是合理的。

这个 m_pGameMap 的每个元素存放一个整数编号,代表图片种类。比如编号 0 代表空位,编号 1 代表第一类图片,编号 2 代表第二类图片,以此类推。之所以用 int 而不是直接用图片对象,是因为算法处理的核心是“编号是否相等”和“是否为空”,不关心图片长什么样。图片的绘制交给 MFC/GDI 层去处理,逻辑层只关心数组里的数值。

报告里还定义了 tagVertex 结构体,用来保存游戏地图中一个点的行号、列号和值信息。这个结构体是消子算法的最小操作单元,我把它理解为“逻辑坐标 + 状态”的封装:

// global.h typedef struct tagVertex { int row; // 行号 int col; // 列号 int value; // 该位置图片的编号,BLANK 表示空 } Vertex;

定义这个结构体的意义在于:消子算法里需要频繁传递“点”的位置信息,如果只用 (row, col) 两个参数传递,接口会很散;如果用数组下标硬编码,又会让代码难以阅读。封装成 Vertex 后,无论是一条直线连通还是两条直线连通,传参都只需要传两个 Vertex,可读性和可维护性都会好不少。

2.2 地图生成的关键约束:偶数重复数与可完全消除性

地图生成不是简单地往 160 个格子里随机填图就完事,它有一个硬性约束:必须保证整张地图可以被完全消除。这个约束的数学基础是:只有两张相同图片才能消除,所以每种图片出现的次数必须是偶数(2 的倍数),否则到最后一定会剩下单张图片无法消除,游戏进入死局。

因此地图生成的第一步不是随机填充,而是计算每种花色的重复数。报告给出的公式是 nRepeatNum = nRows * nCols / nPicNums,即重复数 = 总格数 / 花色数。但这只是第一步,还必须校验以下两个条件:

  1. (行数 × 列数)% 花色数 == 0,否则无法整除,总格数不能被平均分配到每种花色。
  2. 重复数必须能被 2 整除,否则会出现单张图片无法配对的情况。

校验代码我按报告的思路补全如下:

// CGameLogic.cpp 初始化地图 int nRows = 16, nCols = 10; int nPicNums = 20; // 假设用 20 种花色 if (nRows * nCols % nPicNums != 0) { // 异常处理:花色数与地图大小不匹配 AfxMessageBox(_T("地图大小与花色数不匹配!")); return; } int nRepeatNum = nRows * nCols / nPicNums; // 每种花色重复次数 if (nRepeatNum % 2 != 0) { // 异常处理:重复数为奇数,无法完全消除 AfxMessageBox(_T("每种花色的重复数必须为偶数!")); return; } // 按从左到右、从上到下的顺序填入地图 int nIndex = 0; for (int i = 0; i < nRows; i++) { for (int j = 0; j < nCols; j++) { m_pGameMap[i][j] = nIndex / nRepeatNum; nIndex++; } }

这里有个容易忽略的点:按顺序填入后再打乱,而不是直接随机填充。原因是直接随机填充很难控制每种花色的总数恰好等于 nRepeatNum,容易出现某种图多一张、另一种少一张。先构造规则序列再随机洗牌,才能保证分布精确可控。这个思路和洗牌算法(Fisher-Yates)的原理是一致的——先有序,再随机交换。

2.3 随机打乱地图:洗牌算法的一次实际应用

打乱地图的实现在报告里是通过 srand((int)time(NULL)) 和循环交换完成的。以时间作为随机种子是可接受的做法,注意同一秒内的多次调用会产生相同的伪随机序列,所以如果需要多次重排,建议在循环外只调用一次 srand。核心逻辑是遍历每个格子,随机选另一个格子交换:

// CGameLogic.cpp 重排地图 void CGameLogic::DisOrderMap() { srand((int)time(NULL)); int nVertexNum = nRows * nCols; for (int i = 0; i < nVertexNum; i++) { // 随机生成交换目标位置 int nTargetIndex = rand() % nVertexNum; int srcRow = i / nCols, srcCol = i % nCols; int dstRow = nTargetIndex / nCols, dstCol = nTargetIndex % nCols; // 交换两个格子的值 int temp = m_pGameMap[srcRow][srcCol]; m_pGameMap[srcRow][srcCol] = m_pGameMap[dstRow][dstCol]; m_pGameMap[dstRow][dstCol] = temp; } }

参数设计上这里有一个可以优化的点:nRows 和 nCols 应该是 CGameLogic 的成员变量,由构造函数初始化,而不是魔法数字写死在函数里。如果你要复现,建议把地图尺寸参数化,因为关卡模式可能改变地图大小。我一般会把配置集中在一个地方,方便后续扩展。

注意:重排不能改变数组里的元素种类和总数,只改变位置。所以重排前后的地图必须满足元素多重集合完全一致,这个性质在测试时可以用排序对比快速校验。

3. 消子判断的三类算法:从一条直线到三条直线连通的完整推导

3.1 直线连通:行相同看横向,列相同看纵向

消子算法的第一个层次是判断两个顶点是否被一条直线连通。这个逻辑分两种情况:行相同(同一水平线)和列相同(同一垂直线)。行相同时,只需要检查两个点之间所有的格子是否都是空的——注意这里的连通路径是“两个点之间的格子”,不包含两个点本身的位置。

报告里给出的 RowLink() 函数实现的是 X 方向(横向)连通判断:

// 判断两个顶点在水平方向是否连通 bool CGameLogic::RowLink(int m_Map[10][16], Vertex v1, Vertex v2) { // 确保两个顶点在同一行 if (v1.row != v2.row) return false; int col1 = v1.col, col2 = v2.col; if (col1 > col2) { // 保证 col1 <= col2,统一遍历方向 int temp = col1; col1 = col2; col2 = temp; } // 依次判断中间每个格子是否为空 for (int i = col1 + 1; i < col2; i++) { if (m_Map[v1.row][i] != BLANK) { return false; // 中间有障碍物,不能连通 } } return true; }

这里的参数 m_Map[10][16] 是硬编码的数组维度,和报告里的 16×10 地图其实存在行列不一致的问题——这个我在后面的避坑章节会专门讲。逻辑上需要注意的是循环边界:从 col1+1 开始到 col2-1 结束,跳过两个端点本身。如果两个点相邻(col2 - col1 == 1),循环不会执行,直接返回 true,这是正确的结果。

纵向连通 ColLink() 的写法与 RowLink 对称,区别只是把行坐标作为遍历变量。在实际工程里,我会把这两个函数合并成一个 LineLink(Vertex v1, Vertex v2),通过 v1.row == v2.row 和 v1.col == v2.col 判断方向,减少重复代码。但报告里分开写也有一个好处:调试时可以单独测试某个方向的连通性,定位问题更快。

3.2 两直线连通:寻找拐点,判断“L”型路径

当两个点不能通过一条直线直接连通时,算法进入第二个层次:寻找一个拐点,使得 V0 -> 拐点 -> V3 的路径是“先横后竖”或“先竖后横”的 L 型。核心思路是枚举可能的拐点位置。

假设两个顶点为 v1(row1, col1) 和 v2(row2, col2),拐点的候选位置是两个:一个是 (row1, col2),即与 v1 同行、与 v2 同列;另一个是 (row2, col1),即与 v2 同行、与 v1 同列。这两个拐点必须为空,且拐点与 v1 之间的直线连通、拐点与 v2 之间的直线连通,两个条件同时满足才算成功。

报告里给出的 OneCornerLink() 函数签名是 bool CGameLogic::OneCornerLink(int m_Map[10][15], Vertex v1, Vertex v2),核心逻辑我整理如下:

bool CGameLogic::OneCornerLink(int m_Map[10][16], Vertex v1, Vertex v2) { int row1 = v1.row, col1 = v1.col; int row2 = v2.row, col2 = v2.col; // 拐点1:(row1, col2)——与 v1 同行,与 v2 同列 if (m_Map[row1][col2] == BLANK) { // 拐点必须为空,否则路径不成立 if (RowLink(m_Map, v1, Vertex{row1, col2, BLANK}) && ColLink(m_Map, Vertex{row1, col2, BLANK}, v2)) { return true; // v1->拐点1 横向连通,拐点1->v2 纵向连通 } } // 拐点2:(row2, col1)——与 v2 同行,与 v1 同列 if (m_Map[row2][col1] == BLANK) { if (ColLink(m_Map, v1, Vertex{row2, col1, BLANK}) && RowLink(m_Map, Vertex{row2, col1, BLANK}, v2)) { return true; // v1->拐点2 纵向连通,拐点2->v2 横向连通 } } return false; }

这里有一个细节容易踩坑:拐点位置如果恰好等于 v1 或 v2 的位置,也就是两个点本身在同一行或同一列,那实际上是一直线连通的情况,不应该进入 OneCornerLink。好的做法是在调用 OneCornerLink 之前先调用 RowLink 和 ColLink,如果直线能连通就直接返回成功,不走拐点判断。

另一个容易被忽略的点是:拐点的坐标可能超出地图边界。在边界上的两个点,拐点有可能落在 (row1, -1) 这种非法位置。所以严谨的实现里,拐点坐标必须先做边界检查,再判空。

3.3 三直线连通:枚举关键路径与栈的配合

两条直线还连不通时,就要考虑三条直线的情况了,这也是连连看里最复杂的连通方式。三直线连通的特点是路径上有两个拐点,V0 -> V1 -> V2 -> V3,其中 V1 和 V2 是两个拐点。报告里给出的思路是枚举法:从地图第一行(或第一列)开始扫描,找到一条“关键路径”——这条路径是水平(或垂直)方向的一条通线,再判断这条路径与两个顶点之间是否各自连通。

具体分为两个方向上的搜索:

搜索 X 方向(水平关键路径):选择一个行号 row,在这行上设置 V1(row, col0) 和 V2(row, col3),判断 V1 和 V2 之间是否水平连通。如果连通,再判断 V1 与 V0 纵向是否连通、V2 与 V3 纵向是否连通。三个条件同时满足,则存在三条直线连通路径。

bool CGameLogic::TwoCornerLink(int m_Map[10][16], Vertex v1, Vertex v2) { int row1 = v1.row, col1 = v1.col; int row2 = v2.row, col2 = v2.col; // 情况1:寻找水平关键路径,即 V1 和 V2 在同一行的某个位置 for (int row = 0; row < nRows; row++) { // 查找拐点 V1 和 V2 int col = 0; for (col = 0; col < nCols; col++) { if (m_Map[row][col] != BLANK) continue; // 尝试让 V1 在 (row, col1),V2 在 (row, col2) if (LineX(m_Map, row1, col1, col) && LineX(m_Map, row2, col2, col)) { Vertex V1 = {row1, col, BLANK}; Vertex V2 = {row2, col, BLANK}; if (LineY(m_Map, row, row1, col1) && LineY(m_Map, row, row2, col2)) { AddVertex(V1); // 保存拐点到栈中 AddVertex(V2); return true; } } } } // 情况2:寻找纵向关键路径,逻辑对称 for (int col = 0; col < nCols; col++) { if (LineY(m_Map, row1, row2, col) && LineX(m_Map, row1, col1, col) && LineX(m_Map, row2, col2, col)) { Vertex V1 = {row, col1, BLANK}; Vertex V2 = {row, col2, BLANK}; if (LineY(m_Map, row1, row, col1) && LineY(m_Map, row2, row, col2)) { AddVertex(V1); AddVertex(V2); return true; } } } return false; }

这个算法的复杂度是 O(nRows × nCols),因为每个候选行/列都要做常数次连通性检查。实际运行在 16×10 的地图上毫无压力,但如果把地图加大(比如 50×50),这个 O(n²) 的枚举会开始有明显开销,可以考虑用预处理 + 缓存优化,不过课程设计层面不需要。

路径保存用栈的意义:报告里提到使用栈保存连通路径中的关键点:V0、V1、V2、V3。为什么要用栈而不是数组?原因在于路径点保存后还需要回溯给绘图函数画连接线。栈的“后进先出”特性可以让绘图层按路径顺序依次弹出并画线。这里有一个工程实践上的注意点:如果需要在消子动画里逐段画线,建议在栈里额外保存“路径段类型”(是水平段还是垂直段),否则弹出来只知道坐标,不知道先画横线还是竖线。

提示:消子判断的顺序必须是“一条直线 → 二条直线 → 三条直线”,因为后两者会包含前者的情况(比如拐点恰好与端点重合时,三直线就退化为两直线)。按顺序判断可以避免重复计算,也能保证提示功能找到的一定是最短路径。

3.4 胜负判断与提示功能的算法思路

胜负判断相对简单——遍历整个地图,如果所有元素都为空(BLANK),则游戏获胜;否则继续。代码写起来很直接:

// 判断地图是否为空,即所有图片都被消除 bool CGameLogic::IsBlank(int m_Map[10][16]) { for (int i = 0; i < nRows; i++) { for (int j = 0; j < nCols; j++) { if (m_Map[i][j] != BLANK) { return false; // 还有未消除的图片 } } } return true; }

提示功能的本质是在整个地图范围内找到一对可以消除的图片并高亮显示。实现思路很简单但也很有代表性:第一个循环选中一个非空格子,第二个循环选中另一个非空格子,如果两者值相等且能通过消子算法(依次尝试三种连通),就返回这对图片的坐标,让界面层高亮它们。这个算法在最坏情况下的时间复杂度是 O(n² × 连通性检查),在 160 格的地图上做一次完整扫描通常也在毫秒级。如果地图更大,可以考虑维护一个“可消对”的缓存,在每次消子后增量更新,而不是全量重扫。

4. MFC 框架下的界面与交互实现:从对话框到计时器再到进度条

4.1 MFC Dialog 程序框架与 GDI 绘图的配合方式

这份报告的实验环境是 Microsoft Visual Studio 2010,工程类型是 Win32 控制台应用程序。MFC 框架下的连连看运行起来是一个基于对话框的应用程序——游戏主界面就是主对话框,游戏界面嵌入其中。GDI 负责绘制地图、图片和连接线。

在 MFC Dialog 程序中,绘图的核心逻辑是响应 WM_PAINT 消息。使用双缓冲——先在内存 DC 中绘制完整的一帧,再一次性 BitBlt 到屏幕 DC,避免画面闪烁。报告里的 m_dcMem.BitBlt(...) 就是这个双缓冲绘制的核心调用。

// GameDlg.cpp 双缓冲绘制 void CGameDlg::UpdateMap() { // m_dcMem 是内存 DC,m_dcBG 是背景 DC,m_rtGameRect 是游戏区域 // 先将背景绘制到内存 DC m_dcMem.BitBlt(0, 0, m_rtGameRect.Width(), m_rtGameRect.Height(), &m_dcBG, 0, 0, SRCCOPY); // 再在地图对应的位置绘制每张图片 for (int i = 0; i < nRows; i++) { for (int j = 0; j < nCols; j++) { int nPicIndex = m_gameControl.GetMapValue(i, j); if (nPicIndex != BLANK) { // 根据图片编号从图片资源中绘制相应位图 m_dcMem.DrawState(...); // 简化示意 } } } }

唯一要注意的是 UpdateMap 每次更新前必须先 BitBlt 背景,否则上一帧的残留画面会和新画面叠加,造成“鬼影”。报告里在计时器的 OnTimer 回调中先 StepIt() 更新进度条,再 BitBlt 背景,再调 StartGame/UpdateMap/InvalidateRect,这个顺序是对的——先擦背景再画前景。

4.2 计时器、暂停与进度条联动:定时器 ID 的管理技巧

游戏用 SetTimer(PLAY_TIMER_ID, 1000, NULL) 启动一个 1 秒触发一次的定时器,每次触发在 OnTimer 里更新进度条,然后重绘地图。暂停功能则通过 KillTimer 和重新 SetTimer 来实现。这里有一个工程上常见的细节:如果对话框上同时存在多个定时器(比如一个用于计时、一个用于动画),为了区分它们,枚举值可以是:

enum TimerID { PLAY_TIMER_ID = 1, // 游戏主计时 ANIM_TIMER_ID = 2, // 动画计时(可选) };

暂停游戏时只 KillTimer(PLAY_TIMER_ID),保留动画定时器,这样暂停时画面还能有动画效果。这一层的设计虽小,但能让后续的功能扩展变得从容。

进度条 CProgressCtrl 的设置要注意范围:SetRange(0, 300) 对应 5 分钟(300 秒),SetPos 每秒 +1。基本模式限定 5 分钟,时间到且未消完就弹出“很遗憾,时间到了!”提示框,询问是否重新开始。休闲模式则不设限,只要地图消空即胜利。

4.3 帮助对话框 CHelpDialog:滚动条与图片加载的实现说明

帮助对话框是一个富有趣味的小功能——在原对话框上重新插入一个对话框,加载一张帮助图片,用滚动条浏览大图。核心点是:用 LoadImage 从文件加载 BMP 资源,然后在消息响应函数 OnVScroll 里根据滚动条位置移动图片的绘制偏移量。这里报告里提到的一个坑是“无法加载出背景图片”,最后通过设置断点调试发现是路径问题——从外部加载图片如果不带绝对路径,很容易因为工作目录不对而失败。

我一般建议图片资源用资源文件(.rc)而不是外部文件,这样可以避免路径问题:

// 加载资源中的位图,而不是外部文件 HANDLE bmp = ::LoadImage(AfxGetInstanceHandle(), MAKEINTRESOURCE(IDB_HELP_BG), IMAGE_BITMAP, 0, 0, 0);

如果坚持用外部文件,建议先 GetCurrentDirectory 打印当前目录,再决定用相对路径还是绝对路径。

5. 避坑指南:从数组维度不一致到图片加载失败的排查记录

5.1 地图行列数在不同文件里不一致

现象:跑程序时点击某些图片,消子算法的结果和预期不一致,甚至出现越界崩溃。

原因:报告一部分代码写 m_Map[10][15],另一部分写 m_Map[10][16],还有部分写 m_Map[16][10]。地图实际逻辑是 16 行 × 10 列,但函数签名里出现了不同的维度声明,这会在传递数组时导致内存解释不一致。

解决:把所有函数签名统一为 int m_Map[16][10],或者更稳妥地在头文件里定义宏/常量:

#define MAP_ROWS 16 #define MAP_COLS 10 // 所有函数签名统一写成 int m_Map[MAP_ROWS][MAP_COLS]

如果 C++ 版本支持 constexpr,优先用 constexpr 替代宏,作用域更清晰。这个看似小的改动,能避免大量潜在的越界和错位问题。

5.2 图片数量为奇数导致游戏无法全部消除

现象:游戏进行到最后总会剩下一张图片,怎么消都消不掉;或者提示功能始终找不到可消的一对。

原因:地图生成时没有校验每种花色的重复次数是否为偶数。如果某个编号的图片出现了奇数次,最后一定会剩下一张孤独的图。

解决:在生成地图后马上做一次校验,代码如下:

// 统计每个花色出现的次数 std::map<int, int> countMap; for (int i = 0; i < MAP_ROWS * MAP_COLS; i++) { countMap[m_pGameMap[i / MAP_COLS][i % MAP_COLS]]++; } // 校验每个花色都是偶数次 for (auto& pair : countMap) { if (pair.second % 2 != 0) { AfxMessageBox(_T("地图生成失败:图片数量不是偶数!")); return; } }

报告里提到的“图片是奇数的情况”就是在 GameDlg.cpp 调试时通过断点发现的。这个校验动作虽然简单,但也是建议条件允许时保留——因为它不只是在初始生成时需要,重排时也需要。

5.3 拐点搜索包含端点自身,导致错误连通

现象:两条图片明明不相邻,也没有路径可达,却被判定为可消除。

原因:在 TwoCornerLink 的三直线搜索中,没有排除拐点恰好等于 v1 或 v2 的情况。比如 V1 的位置和 v1 重叠时,LineX 的判断就变成了“同一个点是否连通”,这恒为真,会把不该连通的点连起来。

解决:在枚举拐点前,先判断拐点坐标是否等于 v1 或 v2 的坐标,如果相等则跳过。还有一种做法是在连通性判断函数内部排除起点和终点,统一处理。从我的经验看,把“坐标不等于端点”这个约束放在调用者(TwoCornerLink)一层更清晰,因为基础连通函数本身不应该知道调用者的意图。

5.4 重排后地图无解却无法恢复

现象:点击重排按钮后,新的地图依然没有可消除的对子,玩家陷入死局。

原因:重排的本质只是随机交换位置,但它无法保证交换后一定存在可消除的对子。在游戏后期已经消掉大部分图片时,剩下的图片可能聚集在地图的某些边角,无论如何重排,都不存在相邻或可连通的相同图对。

解决:在重排后调用一次“是否有可消对”检测函数,如果没有可消对,则再次重排。为了完美兜底,可以设定一个最大重排次数(比如 100 次),超过后自动生成一张全新地图:

bool CGameLogic::IsDeadLock() { // 遍历所有非空图片对,看是否存在可消除的 for (int i = 0; i < MAP_ROWS * MAP_COLS; i++) { if (m_pGameMap[i / MAP_COLS][i % MAP_COLS] == BLANK) continue; Vertex v1 = {i / MAP_COLS, i % MAP_COLS, m_pGameMap[i / MAP_COLS][i % MAP_COLS]}; for (int j = i + 1; j < MAP_ROWS * MAP_COLS; j++) { if (m_pGameMap[j / MAP_COLS][j % MAP_COLS] == BLANK) continue; Vertex v2 = {j / MAP_COLS, j % MAP_COLS, m_pGameMap[j / MAP_COLS][j % MAP_COLS]}; if (v1.value == v2.value && CanLink(v1, v2)) { return false; // 存在可消除对,不是死局 } } } return true; // 死局 }

检测到死局后自动重排,这是很多商业连连看的标准兜底逻辑。课程设计做到这一层已经属于加分项。

5.5 外部图片加载失败

现象:帮助对话框加载 BMP 图片时只显示空白,没有任何报错。

原因:LoadImage 使用相对路径(比如 "theme\picture\Help1.bmp"),程序运行时的工作目录通常不在工程目录下(VS 调试时可能是工程目录的 Debug 子目录),相对路径找不到文件,加载失败但不报错,直接返回 NULL。

解决:绝不要依赖相对路径。最好的做法是把图片放进资源文件,用 MAKEINTRESOURCE 加载;如果必须用外部文件,先获取进程当前目录并拼接完整路径,或者直接写绝对路径用于调试。也可以判断 LoadImage 返回值是否为 NULL,如果为 NULL 则输出 GetLastError 信息辅助排错。

6. 从复现到验收:边界用例测试与改动日志

要判断这份报告是否真正被你读懂,而不只是在编辑器里把所有代码抄一遍,有一个行之有效的自检清单。对于连连看这个项目,我会按顺序跑以下三个测试:

第一个是“相邻消除测试”。开局后立刻点击任意一对相邻且相同的图片,观察是否符合“一条直线连通”的判断——路径为空、直接消除。这个用例验证的是最基础的 RowLink/ColLink。

第二个是“L 型拐点测试”。找一对图,它们不在同一行或同一列,但可以通过一个空拐点连通,比如 v1(2,3) 和 v2(5,6),拐点(2,6)为空。观察是否消除成功,以及连线是否绘制的是一条折线(先横后纵或先纵后横)。这个用例验证的是 OneCornerLink。

第三个是“Z 型三拐点测试”。找一对图,它们的连通路径需要两个拐点。验证重点是:连线能够带拐点绘制出来,而且路径上没有经过任何有图片的格子。这个用例验证的是 TwoCornerLink 以及栈保存路径的正确性。

从代码可维护性的角度,我会在完成基础功能后额外检查一个点:如果把地图改成 8 行 × 12 列(96 格),是否还能正常生成、消子、重排?如果代码里硬编码了 16 和 10,改地图尺寸就会翻车。所以我在写过一次之后,把地图尺寸全部抽成了常量,现在每次拿到类似课程设计都强制走一遍“改尺寸测试”。

另外一个值得刻意做的事是看消子后的积分计算逻辑是否准确。报告里提到“计算相应的积分”,但没有给出具体的加分规则。我建议在开始游戏时把积分清零,每次成功消一对加 10 分,时间每少 1 秒额外加 1 分(仅基本模式),这样玩家能感知到“消得越快、分越高”,也方便你验收计时功能是否准确配对。

我自己的习惯是每次改完代码,回到第一句话那条跑一遍测试用例清单,而不是按“我觉得应该没问题”来做。有一次就是靠这个习惯发现 ThreeCornerLink 的纵向关键路径搜索少了一个边界条件——有一个地图配置下整行图恰好全空时,会把 (row, -1) 当成拐点塞进栈里,绘图时直接越界。从那以后我每次做路径类算法,都强制走一遍“全空行/列下的边界扫描”测试。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询