2048的撤销键:快照、分数与随机状态的边界
给2048加一个“撤销一步”按钮,看起来只需要保存旧棋盘。但实现时很快会遇到几个问题:得分要不要恢复,最佳纪录要不要回退,无效按键会不会覆盖记录,撤销后再次移动会不会生成同一个方块?
我以《2048·数字漫游》的现有代码为例,检查了这几项行为,并用固定随机数的浏览器测试验证了六个场景。这里讨论的是单步撤销,不是可以反复向前回退的完整历史系统。
保存一次有效移动之前的快照
程序用长度为16的数字数组表示棋盘,用一个数值保存本局分数。有效移动的入口如下:
function move(direction) {
const result = nextBoard(board, direction);
if (!result.changed) return;
previous = { board: board.slice(), score };
board = result.board;
score += result.gained;
spawn();
render();
}
快照保存的是“这次操作之前”的棋盘和分数。计算移动结果时先不修改当前状态,确认发生变化后,才保存快照、应用结果、累加分数并生成新方块。
棋盘元素都是数字,因此这里用 slice() 复制一层数组就足够了。若后续把格子改成包含位置、动画或其他可变数据的对象,就需要重新检查这份复制能否隔离修改。
本局分数恢复,最佳纪录保留
撤销处理只恢复两个字段,然后消耗这份记录:
if (previous) {
board = previous.board;
score = previous.score;
previous = null;
render();
}
例如把两个2合并成4,本局增加4分。撤销后,两个2和原本的本局分数都恢复。最佳纪录则保留这次曾经达到的分数,因为它表示历史达到的最高值,未放进单局快照。
这种区分需要在产品规则中说清楚:撤销恢复当前局面,最佳纪录仍是历史记录。如果希望连最佳纪录一起回退,就要调整保存范围和持久化规则,不能只改界面上的数字。
无效按键不消耗撤销机会
如果棋盘已经贴在左侧,又没有能够合并的数字,再按左键不会产生有效移动。代码在保存快照之前检查 changed,因此不会把旧快照覆盖成当前棋盘,也不会额外生成方块。
测试这条规则时,输入局面必须确实不能继续合并。例如固定随机生成2后,一行已经是 [4, 2, 0, 0],向左无变化;若一行是 [2, 2, 0, 0],再次向左就会合并,它不能用作“无效移动”的测试样例。
撤销不等于重放随机过程
现有快照只包含棋盘和本局分数,没有保存随机数生成器状态。撤销后再次选择同一个方向,随机方块的位置或数值可能改变。
如果产品需要确定性的回放,就要把随机过程纳入设计,例如保存随机种子和生成器状态,或者记录每一步生成的方块。本版本没有实现这些能力,不能把它描述成完整回放。
六项实际检查
我在独立浏览器测试页中加载实际HTML,把随机函数固定为零,以便重复比较局面。检查结果如下:
| 场景 | 实际结果 |
|---|---|
| 两个2合并 | 保存旧棋盘和旧分数,本局增加4分 |
| 点击撤销 | 恢复原棋盘,本局分数回到原值 |
| 撤销产生历史最高分的一步 | 最佳纪录保留 |
| 已经撤销一次 | 快照清空,撤销按钮禁用 |
| 在有效移动后执行一次无效移动 | 棋盘和原撤销记录都不改变 |
| 开始新的一局 | 清空快照、本局分数归零并生成两块 |
六项均符合当前设计。固定随机数用于控制测试局面,不表示验证了生产环境的随机分布。
单步撤销最关键的设计工作,是先决定什么属于当前局面、什么属于历史纪录,以及哪些状态根本没有保存。明确边界后,按钮、快照和测试才能对应到同一套规则。
作品展示:https://gallery.developer.huaweicloud.cn/gallery/0068d35937000000
- 点赞
- 收藏
- 关注作者
评论(0)