单文件2048实战:合并规则、触摸操作与进度保存
一个 2048 小游戏可以成为检验前端基本功的完整练习:状态如何表示、合并是否正确、手机手势如何处理、刷新能否恢复,以及怎样证明实现符合预期。本文基于本次实际编写并在浏览器测试的“竹影 2048”,整理单文件 HTML 的实现思路。页面的 CSS 和 JavaScript 全部内联,不依赖 npm、打包工具或外部图片。开发过程使用了 AI 辅助,文中的验证结果来自实际运行检查。
一、先定义棋盘状态,再编写界面
棋盘使用长度为 16 的一维数组表示,0 代表空格。第 r 行、第 c 列对应的下标为 r * 4 + c。分数、最高分、是否已经选择在达到 2048 后继续游戏,以及上一回合的快照,分别保存在独立变量中。这样可以把“如何计算一次移动”与“如何展示棋盘”分开。
一次移动的顺序是:计算新棋盘,判断是否有变化,保存撤销快照,更新棋盘和分数,生成一个新方块,最后渲染。没有变化的方向不能新增方块;否则用户连续按住一个无法移动的方向,也会把棋盘填满。
二、合并的关键:每个方块一回合只参与一次
以向左移动为例,先去掉一行中的 0,再从左到右扫描相邻值。相等时合并并跳过下一项,不等时直接保留,最后补 0。下面的纯函数只接收一行并返回结果,不读写页面或存储,方便验证边界情况。
function mergeLine(line) {
const values = line.filter(Boolean);
const out = [];
let gained = 0;
for (let i = 0; i < values.length; i++) {
if (values[i] === values[i + 1]) {
out.push(values[i] * 2);
gained += values[i] * 2;
i++;
} else {
out.push(values[i]);
}
}
return {
line: out.concat(Array(4 - out.length).fill(0)),
gained
};
}
最容易写错的输入是 [2, 2, 4, 0]。向左移动后应为 [4, 4, 0, 0],得分增加 4,不能在同一回合继续合成 8。另一个检查是 [2, 2, 2, 2]:结果应为 [4, 4, 0, 0],增加 8 分。
三、让四个方向共用同一个合并算法
向右、向上、向下不必各写一份合并代码。可以先建立每行或每列的四个数组下标。向右和向下时反转下标顺序,按这个顺序提取数据并交给 mergeLine,再将结果写回原下标。这样四个方向只有取数顺序不同,合并规则始终一致。
移动前后逐格比较即可判断 changed。changed 为 false 时直接结束本次操作,不更新分数、不生成方块,也不覆盖撤销快照。生成新方块时先收集所有空位,再随机选择其中一个;本实现以 90% 概率生成 2、10% 概率生成 4。
四、满格不等于游戏结束
游戏结束需要同时满足两个条件:没有空格,且没有水平方向或垂直方向的相邻同值方块。扫描每个格子时,只检查右侧和下侧邻居就足够,避免重复检查。存在任意空位或可合并邻居,就仍可继续。
达到 2048 与无路可走是两个不同状态。前者显示继续游戏按钮;后者显示本局得分和重新开始按钮。继续游戏时只更新 continued 标记,不重置棋盘。
五、手机滑动和键盘输入共用入口
键盘监听方向键与 WASD,将它们映射为 left、right、up、down,再统一调用 move。处理有效方向键时阻止页面滚动,但当焦点位于输入框、文本域或可编辑区域时,不应拦截正常输入。
手机端记录 pointerdown 与 pointerup 的坐标差。当位移小于 24 像素时按点击处理,不移动棋盘;超过阈值后,比较水平和垂直位移的绝对值,较大的轴决定方向。棋盘设置 touch-action: none,只限制棋盘内的默认手势,页面其他区域仍可滚动。pointercancel 发生时清空起点,避免下一次滑动沿用旧坐标。
六、进度保存与撤销需要不同的边界
一次有效移动前,复制当前棋盘、分数和 continued 标记作为上一回合快照。撤销时恢复该快照,并清空它,提供明确的一步撤销能力。最高纪录作为历史统计,不随撤销降低。
棋盘与得分通过 localStorage 保存,读取时放在 try/catch 中。浏览器可能禁用存储,存储内容也可能损坏,游戏应退回到新棋盘而不是白屏。恢复前还要验证:数组长度为 16、元素为 0 或不小于 2 的 2 的幂、分数为非负整数。localStorage 只代表当前浏览器当前来源下的进度,并不提供跨设备同步。
七、用具体检查证明行为,而不是只看页面
本次实际检查了:连续四个同值方块的单次合并、合并后的方块不能二次合并、向右压缩空位、向上与向下的列合并、满格棋盘的结束判定、仍有相邻同值时继续游戏,以及无变化移动不新增方块。
浏览器操作检查还覆盖了键盘移动、撤销恢复原棋盘与分数、刷新后进度恢复,以及 390 像素宽度下页面没有横向溢出。手机布局检查与合成指针事件只能覆盖部分行为,真实设备上的触摸体验仍适合补充测试。
这些检查通过后,可以把 index.html 放到静态网页服务上验证。云端是否成功部署,需要另外确认公网地址能返回正确游戏内容;仅有环境 ID 或仅看到 HTTP 200,都不能代替完整验证。本次游戏的本地实现和浏览器检查已完成,华为云沙箱的公网部署尚未成功,因此本文不将云端部署描述为已完成。
八、这个练习可以继续如何扩展
下一步可以补充移动动画、颜色对比度检查、真实设备触摸测试,以及可重复的随机种子。继续保持计算函数独立,让界面特效不改变游戏规则,通常比一次加入更多功能更容易维护。单文件的价值在于便于阅读和交付;当代码规模增加时,再按状态计算、输入处理和渲染职责拆分即可。
- 点赞
- 收藏
- 关注作者
评论(0)