HarmonyOS7应用《拼棋派对》实战:滑块拼图的图片切块与可解棋盘
滑块拼图有一个容易被忽略的前提:打乱后的图片必须能通过移动空格还原。如果只是把所有图片随机换位,玩家可能面对一盘永远无法完成的题目。《拼棋派对》从完成状态出发,连续执行合法移动生成开局,避开了这个问题。
这一篇先说明图片资源与位置数据,再讲可解打乱和相邻移动。实际效果使用主应用1.0.19、HarmonyOS7 API26模拟器中的“山湖、4×4、滑块”模式。
本文目录
图片块与正确位置分开保存
项目的拼图图片已经预先切成资源,位于 entry/src/main/resources/base/media。运行时按主题、阶数与编号取资源,没有在每次开局时动态裁剪整张原图。增加新主题时,需要准备完整原图和对应阶数的切块资源,不能只修改主题名称。
每个 PuzzleTile 保存id、correctIndex和asset。id描述图片块身份,correctIndex描述它完成时应该位于哪里,数组下标描述它现在在哪里。拖动或交换后,变化的是数组位置,图片块自身的correctIndex保持不变。
createOrderedPuzzle 使用以下命名方式创建完成盘面:
result.push({
id: index + 1,
correctIndex: index,
asset: `app.media.puzzle_${themeId}_${size}_${index + 1 < 10 ? '0' : ''}${index + 1}`
});
例如 puzzle_lake_4_01 对应4×4山湖图的第一块。编号前补零是资源约定的一部分,遗漏会导致部分图片无法找到。页面根据当前数组顺序渲染,完成判断则比较correctIndex与下标,不依赖图片外观。
滑块模式把最后一块替换为空格:id为0,asset为空,correctIndex仍是最后一个位置。因此最终空格也有自己的正确位置。页面用空格样式呈现它,不尝试加载空资源。
从完成状态生成可解盘面
createSlidingPuzzleBoard 先创建完成盘面,再从最后一格开始移动空格。每一步只从上下左右合法相邻位置选一个目标,交换后更新空格索引。
let blankIndex = total - 1;
let previousBlank = -1;
const moveCount = total * 8 + Math.abs(seed % 19);
for (let move = 0; move < moveCount; move++) {
const targets = getPuzzleAdjacentIndices(blankIndex, size);
let targetIndex = targets[
Math.abs(seed + move * 7 + blankIndex * 5) % targets.length
];
if (targets.length > 1 && targetIndex === previousBlank) {
targetIndex = targets[(targets.indexOf(targetIndex) + 1) % targets.length];
}
const temp = result[blankIndex];
result[blankIndex] = result[targetIndex];
result[targetIndex] = temp;
previousBlank = blankIndex;
blankIndex = targetIndex;
}
这段代码来自当前实现。每一次交换都可以逆向执行,因此生成结果必定存在一条返回完成盘面的路径。尽量不立即走回上一空位,可以减少一来一回抵消的移动。如果最终碰巧回到完成状态,函数再做一步合法移动,避免开局直接完成。
交换模式允许任意两块互换,它使用另一个打乱函数 createPuzzleBoard。两种模式的可达状态集合不同,不能把交换模式的随机排列直接用于滑块模式。
用行列检查相邻关系
在4×4盘面中,索引3和4虽然相差1,却分别位于第一行末尾与第二行开头,并不相邻。只写 Math.abs(first - second) === 1 会允许跨行移动。
项目 getPuzzleAdjacentIndices 先换算行列,再根据边界追加上下左右邻居。下面展示同一思路的精简形式:
const row = Math.floor(index / size);
const col = index % size;
const result: number[] = [];
if (row > 0) { result.push(index - size); }
if (row < size - 1) { result.push(index + size); }
if (col > 0) { result.push(index - 1); }
if (col < size - 1) { result.push(index + 1); }
arePuzzleNeighbors 检查目标是否属于这个集合。生成盘面与玩家移动共同使用邻居函数,规则不会出现“打乱时允许、玩的时候不允许”的不一致。
合法移动后更新哪些状态
moveSingleSlidingTile 先查空格,点击空格本身或不相邻图片时只给提示,不增加步数。合法移动时复制图片块数组,交换目标与空格,再更新空格位置、步数和归位数。
const tiles = this.swapTiles(this.singleTiles, index, blankIndex);
const nextSteps = this.singleSteps + 1;
this.singleTiles = tiles;
this.singleSelectedIndex = findPuzzleBlankIndex(tiles);
this.singleSteps = nextSteps;
const completed = countCompleted(tiles);
this.singleCompleted = completed;
this.singleBoardVersion = this.singleBoardVersion + 1;
if (completed === this.puzzleTotal()) {
this.finishSinglePuzzle();
}
复制数组后再赋值,使页面状态变化更明确。步数表示成功移动次数,不是手指点击次数。无效点击可以触发反馈或高亮,但不应把玩家的成绩变差。
countCompleted 遍历所有块,统计correctIndex等于当前位置的数量。因此DONE不是“距离最短解还有多少步”,也不是图片相似度,而是已经归位的比例。一块刚好归位又被移走时,完成率可以下降,这符合当前计算方式。
在模拟器中比较有效与无效点击
进入单人拼图,在“设置”里确认山湖、4×4、滑块,再返回拼图。本次初始空格位于第二行第二列,索引5。点击左上角索引0,它不在空格邻居集合内,界面提示“只能移动空格上下左右相邻的图片”,STEP保持0。
再点击空格上方的索引1,图片移入空格,空格到达第一行第二列,STEP变为1。随后打开“原图”,可以对照山湖完整画面判断图片块的归属。
动图中,非法点击保持0步,合法移动变为1步,最后打开原图参考。本次验证了移动规则和原图查看,整盘还原不属于这组操作的测试范围。
可解、难度与完成率的区别
从完成状态打乱可以保证可解,但不能保证每局难度一样。移动次数不等于最短解长度:随机路径可能绕圈,很多步也可能生成很容易的局面。如果后续希望给出精确难度,应增加局面评价或求解距离,而不是只根据打乱次数显示难度等级。
图片主题也会影响体验。相似的天空或水面块需要更强的边界信息,资源切块最好保持一致尺寸与完整衔接。当前用预切资源换取稳定显示,代价是每个阶数都需要相应图片;若未来支持用户照片,再考虑运行时切图、缓存与内存管理。
这篇把三个条件连到一起:图片有固定身份,移动遵守邻接关系,开局来自合法路径。下一篇复用这些规则,为两名玩家分别保存进度和用时。
源码入口:GameLogic.ets 的 createOrderedPuzzle、createSlidingPuzzleBoard、getPuzzleAdjacentIndices;Index.ets 的 moveSingleSlidingTile。资源与ArkUI开发资料见HarmonyOS官方文档。
更多推荐
所有评论(0)