☰
微信小程序连连看源码:连通判定与棋盘算法全解析
2026/10/15 13:45:25 网站建设 项目流程

简介:连连看小程序游戏源码是一份适合C#初学者的完整小型休闲游戏项目,面向想通过实战掌握桌面游戏开发的学习者。项目基于Windows Forms实现图形界面,涵盖方块绘制、点击交互、消除匹配等核心玩法,可帮助理解面向对象编程、事件处理、GUI布局以及计时与动画等关键技能。资源共42个文件,压缩包约4.57MB,以bmp图片、h/cpp源代码、txt说明文档、mp3音效及ico图标等为主,同时包含项目工程文件(如sln、vcproj)与可执行exe,便于直接运行或对照学习。已有759人学习下载。通过阅读源码与排查匹配算法、状态管理等模块,读者能掌握二维数组存储、匹配检测逻辑以及程序调试方法,是一份理论结合实践、上手门槛较低的学习资料。

1. 连连看小程序:一套带完整连通判定与动效的微信小游戏源码

先抛一个和大多数直觉相反的点:写连连看最花精力的不是“找到两张相同的图”,而是“判断两张图能不能连”。两个图案相同的格子,之间最多允许两个拐点,整条路径不能穿过其它卡片,这个判定写不严谨,轻则边缘区域出现“看上去能连却被判不行”,重则玩到后半场所有配对清掉之后代码直接卡在死局检测里出不来。这套源码把这一整条链路补齐了:棋盘生成、0/1/2 拐点连通判定、死局检测、提示与洗牌,界面走原生小程序组件路线,自带点击高亮、消除连线和计时计分。适合两类人:一类想拿小程序做小游戏功能、顺着项目练逻辑拆分;另一类接外包或做毕设,需要一套能直接导入跑通再按需求裁剪的游戏模块。拿到手先别急着改界面,把utils/path.js吃透,后面怎么改都不慌。

2. 工程结构与技术选型:三层分离的原生小程序骨架

2.1 为什么选原生小程序而不是 uni-app 或小游戏引擎

做连连看这类棋盘游戏,技术选型的第一原则是别让渲染框架绑住算法。这套源码用的是原生微信小程序,WXML 画界面、WXSS 写样式、JavaScript 写业务,没有引入 uni-app,也没上 Cocos 之类的小游戏引擎。理由很直接:连连看的核心逻辑是棋盘状态计算和连通路径搜索,这两块全是纯 JS 函数,跟视图层没有关系;玩家交互部分无非是点击、高亮、消除、连线,原生小程序的 setData 和数据绑定完全撑得住。

反过来看,引入跨端框架后每次迭代都要多过一层编译,调试报错时定位链路会明显拉长;引擎方案在包体和内存开销上又偏大,启动速度对轻量棋牌类并不友好。在包体限制下,原生小程序把页面代码和资源压缩完很容易控制在小体积内,这也是很多轻量级游戏类小程序坚持原生写法的主要原因。项目规模在这里不值得为框架额外付复杂度,原生的可读性和可控性是最好的。

2.2 目录结构:棋盘、路径、音效、页面各司其职

这套源码的工程结构不长,但模块边界分得很清楚:

├── app.js 小程序入口,全局启动逻辑 ├── app.json 全局配置,注册页面和窗口样式 ├── app.wxss 全局样式 ├── pages/ │ └── game/ │ ├── game.wxml 游戏主界面:棋盘、计分栏、控制按钮 │ ├── game.wxss 界面样式,含卡片网格布局 │ ├── game.js 页面逻辑:点击、消除、计时、洗牌 │ └── game.json 单页配置 ├── utils/ │ ├── board.js 棋盘生成、洗牌、重排、死局检测 │ ├── path.js 连通判定核心算法 │ └── audio.js 音效封装(提示音、消除音) ├── assets/ │ ├── images/ 图案素材与背景图 │ └── audio/ 音效文件 └── config/ └── game.js 关卡参数:行列数、图案种类数、初始时间

页面部分只做一件事:把用户操作翻译成对 utils 模块的调用,再把结果同步到界面。utils 下面三个文件都不依赖页面上下文,可以单独拿出来做单元测试,这是最值得保留的设计。config 独立成目录而不是散落在 game.js 里,是为了后续改成多关卡模式时不碰页面逻辑。

音效单独封装也是同一个思路。消除音、点击音、洗牌音被散落在不同函数里,如果每个调用点都自己创建 InnerAudioContext,会出现多个音频实例叠放、退出页面后无法统一释放的问题。统一成 audio 单例之后,调用处只需要audio.play('clear'),逻辑就干净了。实际接外包或者改毕设时,这种结构最省事:除非要加新玩法,否则你的改动基本只会落在utils/path.js和config/game.js两个文件上,UI 层的其它部分几乎不用碰。

2.3 棋盘数据模型:为什么外层要包一圈 0

很多第一次写连连看的人会把棋盘定义成和视觉一致的二维数组,R 行 C 列,0 表示空位,非 0 表示图案编号。这个模型能跑,但边缘卡片的连通判定写起来非常别扭——当一张牌想绕外圈走时,代码得先判断目标格子是否在棋盘范围外,再决定要不要把它当成空位。

更省事的做法是直接给数组外层包一圈 0。玩家看到的是 8 行 10 列,但代码里存的是 10 行 12 列的二维数组,所有可见格子从索引 1 开始,外圈永远是 0。这样处理有两个收益:边缘卡片天然拥有一条外围通路,判定函数里不需要任何越界分支;2 折路径枚举中转行列时直接遍历 0 到 board.length,外圈会被当成合法路径的一部分参与计算。

// config/game.js module.exports = { rows: 8, // 可见棋盘行数 cols: 10, // 可见棋盘列数 patternCount: 5, // 图案种类数,必须满足 rows*cols/2 能被它整除 baseTime: 180 // 初始倒计时(秒) }; // utils/board.js const { rows, cols, patternCount } = require('../config/game'); function shuffle(arr) { for (let i = arr.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [arr[i], arr[j]] = [arr[j], arr[i]]; } } function createBoard(logicalRows, logicalCols, patternCount) { const total = logicalRows * logicalCols; const pool = []; // 每种图案生成两次,保证成对出现 for (let i = 0; i < total / 2; i++) { const type = (i % patternCount) + 1; pool.push(type, type); } shuffle(pool); // 外圈 +1,可见格子从索引 1 开始 const board = Array.from( { length: logicalRows + 2 }, () => Array(logicalCols + 2).fill(0) ); let idx = 0; for (let r = 1; r <= logicalRows; r++) { for (let c = 1; c <= logicalCols; c++) { board[r][c] = pool[idx++]; } } return board; }

参数说明:rows 和 cols 表示玩家可见的行列数,createBoard 返回的数组实际行数是 rows + 2、列数是 cols + 2。patternCount 有一个容易被忽略的约束——total / 2 必须是它的整数倍,否则会出现某种图案多一对、另一种少一对,开局素材就不均衡。8x10 的棋盘配 5 种图案,总共 40 对牌,每种图案正好 8 对,是比较稳的配置。初始化之后,board[r][c] 存的是图案编号 1 到 patternCount,外圈所有位置全是 0,后面所有算法都基于这个“永远不越界”的数组工作,这就是它作为数据契约的价值。

洗牌之后如果觉得开局连续出现相邻同图案、一上来就白送几对,可以在生成后补一轮“上下左右四个方向没有相同图案”的局部重排。这属于体验优化,不影响规则正确性,但玩家对第一屏的印象往往决定了留存,值得做。

3. 核心算法拆解:从 0 拐到 2 拐的连通判定完整链路

3.1 先定义清楚三件事:格子、路径、拐点

连连看的规则一句话讲完:两张图案相同的卡片,如果能通过一条水平或垂直的折线连接,折线段除端点外不经过任何卡片,且拐点不超过两个,就可以消除。定义里藏着三个工程化问题:怎么描述一个格子、怎么描述一条路径、怎么描述拐点。

这套源码把格子抽象成{ r, c }对象,r 是行、c 是列,对应 board 数组下标。路径则抽象成坐标数组:[p1, p2]表示直线,[p1, corner, p2]表示单拐,[p1, mid1, mid2, p2]表示双拐。这个数组在代码里身兼两职:既是连通判定的返回值,也是消除动画里 canvas 连线的数据源。判定和动画共用一份数据,省掉了“判定过了但动画画不出来”这类不同步问题。

3.2 直线段检查:isLineClear 的实现

任何拐弯路径最终都会拆成若干条直线段,所以第一个要写的是“两点之间同行或同列且无遮挡”,它是整个算法的地基。

// utils/path.js 中的基础工具函数 function isLineClear(board, r1, c1, r2, c2) { // 同行时逐列检查中间格子 if (r1 === r2) { const minC = Math.min(c1, c2); const maxC = Math.max(c1, c2); for (let c = minC + 1; c < maxC; c++) { if (board[r1][c] !== 0) return false; } return true; } // 同列时逐行检查中间格子 if (c1 === c2) { const minR = Math.min(r1, r2); const maxR = Math.max(r1, r2); for (let r = minR + 1; r < maxR; r++) { if (board[r][c1] !== 0) return false; } return true; } return false; // 既不同行也不同列,不满足直线段前提 }

参数说明:board 是第 2 章里外层包 0 的棋盘,r1/c1、r2/c2 是两个端点的逻辑坐标。函数只检查两个端点中间有没有挡路卡,不管是空格还是越界外推的空位,只要是非 0 就返回 false。循环从 min+1 走到 max-1,天然排除了端点,这里去掉端点判断很关键——端点存的必然是有效图案,如果把端点也纳入循环,这个函数永远返回 false。

3.3 0 折与 1 折判定:先试直连,再试两个候选拐点

有了直线段检查,连通判定就能从简单情况往复杂情况叠加。先把 0 折和 1 折实现单独拆开看:

// utils/path.js 中的连通判定入口(2 折部分在下一节拼接) function canConnect(board, p1, p2) { const r1 = p1.r, c1 = p1.c; const r2 = p2.r, c2 = p2.c; // 空格不能参与消除,图案不同不能消除 if (board[r1][c1] === 0 || board[r2][c2] === 0) return null; if (board[r1][c1] !== board[r2][c2]) return null; // 0 折:同行或同列直接连通 if ((r1 === r2 || c1 === c2) && isLineClear(board, r1, c1, r2, c2)) { return [{ r: r1, c: c1 }, { r: r2, c: c2 }]; } // 1 折:拐点只可能是 (r1, c2) 或 (r2, c1) const corners = [ { r: r1, c: c2 }, { r: r2, c: c1 } ]; for (const corner of corners) { if (board[corner.r][corner.c] !== 0) continue; const seg1 = isLineClear(board, r1, c1, corner.r, corner.c); const seg2 = isLineClear(board, corner.r, corner.c, r2, c2); if (seg1 && seg2) { return [{ r: r1, c: c1 }, corner, { r: r2, c: c2 }]; } } return null; // 2 折枚举拼接在这个 return 之前 }

先做两个前置判断,过滤掉无意义的调用。0 折一行代码覆盖同行和同列。1 折的场景可以想象成一个矩形,拐点只可能落在两个对角位置,也就是 (r1, c2) 或 (r2, c1)。每个候选拐点要满足三个条件:自身是空位、A 到拐点直线通畅、拐点到 B 直线通畅。这里board[corner.r][corner.c] !== 0的提前过滤很划算,能跳过大量不必要的线段扫描。

返回 null 表示不可连通;返回数组则表示连通,数组里每个点都是路径关键节点。后面画连线时直接把这个数组往 canvas 上描线,不需要再算一次路径。

3.4 2 折判定:枚举中转行与中转列

2 折的判定要靠枚举。思路是:把两折路径拆成三段,先枚举一个中转行 r,让路径先走竖线 (r1,c1) 到 (r,c1),再走横线 (r,c1) 到 (r,c2),最后走竖线 (r,c2) 到 (r2,c2);行不行再枚举中转列 c,换成先横后竖再横。这一节单独拎出来讲,是因为它最容易踩边界,拼进 canConnect 时把下面代码插在 3.3 的 return null 之前即可。

// 2 折:枚举中转行,走 竖 -> 横 -> 竖 for (let r = 0; r < board.length; r++) { if (board[r][c1] !== 0 || board[r][c2] !== 0) continue; if (isLineClear(board, r1, c1, r, c1) && isLineClear(board, r, c1, r, c2) && isLineClear(board, r, c2, r2, c2)) { return [ { r: r1, c: c1 }, { r: r, c: c1 }, { r: r, c: c2 }, { r: r2, c: c2 } ]; } } // 2 折:枚举中转列,走 横 -> 竖 -> 横 for (let c = 0; c < board[0].length; c++) { if (board[r1][c] !== 0 || board[r2][c] !== 0) continue; if (isLineClear(board, r1, c1, r1, c) && isLineClear(board, r1, c, r2, c) && isLineClear(board, r2, c, r2, c2)) { return [ { r: r1, c: c1 }, { r: r1, c: c }, { r: r2, c: c }, { r: r2, c: c2 } ]; } } return null;

注意枚举范围刻意写成了 0 到 board.length 和 0 到 board[0].length,而不是 1 到 length-1。因为棋盘外层包了一圈 0,中转移行落在最外面那一行时,实际上就是在走外圈虚拟通路,边缘卡片绕路正是靠这个范围覆盖的。循环里先检查两个中转点是否为 0 再继续,比直接调三次 isLineClear 省得多。

在 10x12 棋盘规模下,最坏情况每对组合要跑几十段直线段扫描,总耗时也就是毫秒级,完全没必要为这个量级引入缓存或 A* 之类的搜索算法,枚举方案是这一场景下的最优简单解。

3.5 死局检测与提示:一个函数两种用途

死局检测和提示功能共享同一个底层逻辑:找到任意一对可消除的卡片,找不到就说明当前局面无解。

// 返回任意一组可消除的 {r,c} 坐标,找不到返回 null function findHint(board) { // 只遍历可见区域,索引 1 到倒数第二行/列 for (let r1 = 1; r1 < board.length - 1; r1++) { for (let c1 = 1; c1 < board[0].length - 1; c1++) { if (board[r1][c1] === 0) continue; for (let r2 = r1; r2 < board.length - 1; r2++) { for (let c2 = (r2 === r1 ? c1 + 1 : 1); c2 < board[0].length - 1; c2++) { if (board[r2][c2] !== board[r1][c1]) continue; const path = canConnect(board, { r: r1, c: c1 }, { r: r2, c: c2 }); if (path) return [{ r: r1, c: c1 }, { r: r2, c: c2 }]; } } } } return null; }

嵌套循环里 r2 从 r1 开始、同行时 c2 从 c1+1 开始,保证每对组合只被检查一次,不会出现对称重复。这里的双重循环是 O(N²),但落在棋盘规模上也就是几十万次量级,普通手机都扛得住。返回 null 时触发自动洗牌,洗牌后如果还是死局就再洗,直到有解为止;提示按钮拿到返回值后高亮两张牌,玩法层直接复用同一个函数。

4. 界面交互与动效:点击、消除连线和计时计分的反馈链路

4.1 为什么用 view 网格而不是 canvas 直接画

棋盘渲染有两条路:纯 WXML 网格,每个格子一个 view;整块棋盘用 canvas 绘制,点击时做坐标换算。这套源码选了前者。8 行 10 列的可视格子只有 80 个,WXML 渲染压力很小,点击事件天然返回当前格子的索引,逻辑干净。canvas 方案更适合更大规模的棋盘或大量精灵动画场景,对连连看这种格子本身就是全部交互对象的游戏,反而要多写一层坐标转换,收益不大。

WXML 渲染的关键是把二维数组拍平成一维 view 数组。setData 对二维数组做局部更新很难,每次更新某个格子都得先深拷贝再整体赋值;一维数组通过索引直接定位,性能消耗和代码清晰度都更好。

// pages/game/game.js 中的展平逻辑 const ICONS = { 1: '/assets/images/icon_1.png', 2: '/assets/images/icon_2.png', 3: '/assets/images/icon_3.png', 4: '/assets/images/icon_4.png', 5: '/assets/images/icon_5.png' }; function flattenBoard(board) { const cells = []; for (let r = 1; r < board.length - 1; r++) { for (let c = 1; c < board[0].length - 1; c++) { const value = board[r][c]; cells.push({ row: r, // 保留棋盘坐标,点击回调用 col: c, value, icon: value ? ICONS[value] : '', selected: false, hint: false }); } } return cells; }

每个 cell 对象里的 row 和 col 是原始棋盘坐标,不是展平后的下标。这样点击事件通过><view class="board"> <view wx:for="{{cells}}" wx:key="index" class="cell {{item.value ? 'filled' : 'empty'}} {{item.selected ? 'selected' : ''}} {{item.hint ? 'hint' : ''}}" >onCellTap(e) { const { row, col } = e.currentTarget.dataset; const board = this.data.board; if (board[row][col] === 0) return; const first = this.data.selected; if (!first) { this.setData({ selected: { row, col } }); return; } // 再点一次同一个格子,取消选择 if (first.row === row && first.col === col) { this.setData({ selected: null }); return; } const path = canConnect(board, first, { row, col }); if (!path) { // 换一张重选,旧选中状态被覆盖 this.setData({ selected: { row, col } }); } else { this.eliminate(first, { row, col }, path); } }

dataset 里取出的 row、col 在小程序里可能是字符串,调用 canConnect 前最好先 Number() 转成数字,避免first.row === row这种比较因为类型不一致而失效。选第二张卡时如果不可消除,把选中位直接移到新卡上,这种交互比“保持第一张不变”更顺手,玩家不用先取消再重选。

4.3 消除流程与连线动画:canvas 覆盖层与坐标换算

消除是一个复合动作:更新 board 中两个格子的值为 0、更新 cells 中对应两项、清空选中状态、累加得分、在棋盘上画一条路径连线。先更新数据再画动画,保证棋盘状态和视觉表现一致。

eliminate(p1, p2, path) { const board = this.data.board; board[p1.r][p1.c] = 0; board[p2.r][p2.c] = 0; const cells = this.data.cells.map((cell) => { if ((cell.row === p1.r && cell.col === p1.c) || (cell.row === p2.r && cell.col === p2.c)) { return { ...cell, value: 0, icon: '', selected: false }; } return { ...cell, selected: false }; }); this.setData({ board, cells, selected: null, score: this.data.score + 2 }); this.drawPath(path); }

参数说明:p1 和 p2 是两端格子的坐标对象,path 是 canConnect 返回的完整路径数组。setData 里一次性更新四个字段,避免多次调用触发多次渲染。board 更新在前,cells 更新在后,画动画时视觉已经切换成空位状态。

连线动画单独用一个 canvas 层铺在棋盘上方,不参与 WXML 布局。画线要做一次坐标换算:逻辑坐标带外圈偏移,第 r 行第 c 列的格子在画布上的中心点坐标是((c - 1) * cellSize + cellSize / 2, (r - 1) * cellSize + cellSize / 2)。

drawPath(path) { const ctx = wx.createCanvasContext('linkLayer'); const cellSize = this.data.cellSize; ctx.setStrokeStyle('#ff8c00'); ctx.setLineWidth(6); ctx.setLineJoin('round'); ctx.beginPath(); path.forEach((point, idx) => { const x = (point.c - 1) * cellSize + cellSize / 2; const y = (point.r - 1) * cellSize + cellSize / 2; if (idx === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); }); ctx.stroke(); ctx.draw(); }

cellSize 是页面 onReady 时根据屏幕宽度和棋盘列数算出来的固定值,不要在每次绘制时重新计算,否则真机上容易出现因异步取宽度导致的坐标抖动。连线颜色和粗细建议收敛成配置项,后续换皮肤只动设置不碰逻辑。

4.4 计时、提示与洗牌:辅助模块的接入位置

计时器用 setInterval 每秒更新一次剩余时间,时间归零时先清除定时器再进入结算逻辑。最容易踩的坑是页面切后台时 setInterval 仍在跑,玩家切出去几秒回来发现倒计时少了一截,所以要在 onHide 里暂停、onShow 里恢复,定时器实例也要存成页面属性方便清理。

startTimer() { this.clearTimer(); this.timer = setInterval(() => { const remain = this.data.remain - 1; if (remain <= 0) { this.clearTimer(); this.onGameOver(); return; } this.setData({ remain }); }, 1000); } clearTimer() { if (this.timer) { clearInterval(this.timer); this.timer = null; } }

提示按钮调用 findHint 拿到一对坐标后,把对应 cell 的 hint 字段置 true,300 毫秒后自动清除。洗牌按钮则重新收集棋盘上所有非 0 图案,Fisher-Yates 打散后按原空位结构填回去。这两个功能往深了说都是在操作 board 之后再刷新 cells,页面逻辑只做调用和 setData,不要自己重复实现洗牌逻辑,否则容易和 createBoard 里的 shuffle 出现行为不一致。

5. 运行与避坑:开发者工具联调与四条高频问题排查

5.1 三个步骤把项目跑起来

拿到源码后第一步是打开微信开发者工具,点“导入项目”,选择源码根目录,也就是包含 app.json 的那一层。AppID 可以先选测试号,等后面要真机预览再换成自己的正式 AppID。导入后工具会自动编译,如果页面没有立刻出现,先看编译日志里有没有文件路径引用错误,这类问题九成出在 config 或 assets 的路径大小写不一致上。

第二步是完整点一遍流程:开局、消除一对、触发一次提示、用一次洗牌、把时间耗尽看结算。不是随便点点,而是把四条主链路都走通,因为这几个功能在真实对局里会交叉调用,任何一条链路残了都会以奇怪的 bug 形态暴露出来。第三步是点“预览”生成二维码用真机扫码实测,重点观察 canvas 连线动画和倒计时在页面切后台时的表现,模拟器的表现代表不了真机。

真机上如果发现棋盘格子大小不对,优先检查 cellSize 是不是在拿到窗口尺寸之前就计算了。正确顺序是先获取窗口可用宽度,再算每个格子的像素边长,不要在 app.js 启动阶段用固定值算。调试时打开真机调试的 vConsole 看 console 输出,重点跟踪 canConnect 的返回值和 setData 的调用频率,这两个地方是排查问题的第一现场。

提示:vConsole 里能看到每次 setData 的耗时,如果消除一次动画卡顿超过 100ms,先怀疑是不是整个 cells 数组被重建,而不是动画本身的问题。

5.2 避坑记录:四条从调试台上摸出来的血泪经验

  1. 棋盘边缘的卡片永远配对不上

现象:把最外圈的卡片消掉几张后,剩下的边缘卡视觉上能绕路连,但点击后却提示不可消除。

原因:棋盘数组没有做外圈 +1 偏移,连通判定访问边界外坐标时要么越界,要么把外圈当挡路牌处理。

解决:统一改用外层包 0 的棋盘模型,所有格子从索引 1 开始,判定函数不做任何边界判断。这是个全局改动的决定,越早定下来后面踩坑越少。

  1. 洗牌后出现开局即死局

现象:新一局铺完牌,提示按钮第一次点击没有反应,手动点牌也找不到任何可消除对。

原因:生成棋盘时只保证了图案数量成对,没检查当前局面是否存在至少一组可消除对。某些卡片分布下会天然出现无解局,概率不高但一旦碰到,玩家会认为游戏坏了。

解决:createBoard 生成后立刻调用 findHint,返回 null 就重新洗牌。这一步和玩家手动洗牌共用同一个 reshuffle 函数,保证两条路径的行为一致。

  1. 提示功能第一次点有用,第二次点了没反应

现象:第一次点提示能高亮一对卡片,清除高亮后再点第二次没有反馈;偶尔高亮的卡片点击后选中状态错乱。

原因:提示高亮复用了 selected 字段,第二次调用时状态机认为已经选中了第一张卡,无法再开放新提示。高亮和选中混在一起,是典型的状态模型污染。

解决:给 cell 单独加 hint 字段,提示高亮只管 hint,不碰 selected;点击卡片时先清理 hint 再处理选中逻辑。状态机两个状态互相独立后,逻辑就顺了。

  1. canvas 连线在真机上闪没

现象:模拟器里消除连线正常,真机上一闪而过,有时根本看不清路径。

原因:drawPath 里的 ctx.draw() 回调时机在真机上不稳定,后续清理画布的 setTimeout 可能在新 canvas 实例创建后执行了旧的上下文,清除顺序错乱。

解决:把 canvas 实例和上下文创建收敛到页面初始化阶段,动画结束后在 ctx.draw 的回调里清理画布,而不是用 setTimeout 兜底。真机上 canvas 的行为比模拟器敏感,不要在动画时机上做赌运气式的实现。

6. 二次开发:从固定棋盘到多模式玩法的改造思路

6.1 棋盘参数化:把写死的配置拆到 config

目前 rows、cols、patternCount 已经独立在 config/game.js 里,下一步可以把它升级成一整套关卡表。每关指定行列数、图案种类和时间限制,通关后跳到下一关:

// config/levels.js module.exports = [ { id: 1, rows: 6, cols: 8, patternCount: 3, timeLimit: 120 }, { id: 2, rows: 8, cols: 10, patternCount: 5, timeLimit: 180 }, { id: 3, rows: 10, cols: 12, patternCount: 6, timeLimit: 240 } ];

关卡配置里最容易被忽略的还是那个整除约束:patternCount 必须能整除 rows * cols / 2。新增关卡时先心算一遍,不然会出现某关某些图案数量奇偶不匹配,最后剩一堆消不掉的残局。配置化改造完成后再做排行、成就这类外围系统,页面逻辑的压力会小很多。

6.2 三个值得优先动的扩展点

扩展点改动位置需要动到的函数/字段预期效果
限时/步数双模式pages/game/game.jsstartTimer、moveCount同一套棋盘跑两种规则
道具系统(提示/洗牌/暂停)控制栏布局 + game.jsfindHint、reshuffle、暂停状态引入策略取舍,给玩法加深度
多主题皮肤config/theme.js + assets/imagesICONS 映射换皮不换逻辑,降低运营成本

三个扩展点里道具系统最值得优先做,因为它和现有代码的耦合最自然:提示就是 findHint,洗牌就是 reshuffle,暂停只需要在计时器上加一层状态开关。给卡片加“锁定”这类新状态时,注意别把锁定信息直接揉进 board 数组,正确做法是建一个独立的 Set 存锁定卡片坐标,渲染时的 cells 里再合并这份数据,避免每次 setData 都带着无关字段。

我自己的习惯是,每次改完棋盘相关逻辑,先把 config 里的 rows 调小到 2x2,手工摆一对典型可消除卡,跑一遍 canConnect 确认 0 拐、1 拐、2 拐三种路径都返回正确,再调回正常配置继续接新功能。这套源码把路径判定和页面交互隔离得比较干净,验证核心算法时完全可以不碰 UI,直接用 node 跑 utils/path.js 就行。希望帮到你。

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

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

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

立即咨询