← 所有文章

我如何在一个周末做出 2048 —— 完整的游戏循环

2026-06-29

我最近给 ToolKoala 加了 2048,最让我惊喜的是这个游戏的核心居然这么。动画和打磨确实费时间,但规则——滑动、合并、生成、判断游戏结束——大约六十行就装得下。下面讲讲整个循环是怎么转起来的。

棋盘只是一个扁平数组

网格是 4×4,但我把它存成一个由 16 个数字组成的扁平数组,其中 0 表示空:

[2, 0, 0, 4,
 0, 2, 0, 0,
 0, 0, 0, 0,
 4, 0, 0, 2]

扁平数组容易复制、容易比较、也容易存进 localStorage。第 r 行、第 c 列就落在下标 r * 4 + c

一个函数干掉了真正的活儿

每一次移动——左、右、上、下——对一行四个方块来说都是同一种操作:把所有东西推向一端,并把相等的相邻方块合并一次。这就是一个函数:

function compress(line) {
  const arr = line.filter(v => v)   // 去掉那些 0
  const res = []
  let gained = 0
  for (let i = 0; i < arr.length; i++) {
    if (arr[i] === arr[i + 1]) {     // 两个相等的方块相邻 → 合并
      res.push(arr[i] * 2)
      gained += arr[i] * 2
      i++                            // 跳过刚刚被合进去的那个
    } else {
      res.push(arr[i])
    }
  }
  while (res.length < 4) res.push(0) // 补 0 凑回长度 4
  return { line: res, gained }
}

合并之后的那句 i++ 就是阻止 4 4 4 4 塌缩成单个 16 的规则。它会变成 8 8——每个方块每次移动最多只合并一次。

四个方向由一个方向推导

我只写了"向左滑"。另外三个都复用它:

  • —— 把这一行翻转,向左压缩,再翻转回来。
  • 上 / 下 —— 按而不是按行来读取棋盘,然后套用同样的左/右逻辑。

所以一次移动就是抓出每一行或每一列,看情况翻转一下,跑 compress,再写回去。没有给每个方向键单独写代码——区别只在于我读哪条线,以及要不要把它翻过来。

先生成新方块,再看你是不是卡死了

在一次真正改变了棋盘的移动之后,会有一个新方块出现在某个随机的空格里——大多数时候是个 2,偶尔是个 4。然后我会检查是否还存在任何可行的移动:还有空格吗?还有任意两个相等的相邻方块吗?如果都没有,那就是游戏结束。

function canMove(g) {
  if (g.includes(0)) return true            // 有空格 → 可以
  // 任意相等的水平/垂直相邻方块 → 说明还能合并
  // ...用一个小循环来检查...
  return false
}

那个"棋盘到底有没有真的变?"的检查很关键:如果一次移动什么都没改变,你就不该白白得到一个新方块。顶着墙壁按必须是个空操作。

那个让我学会用 ref 的 bug

第一版是在按键处理器里直接从 React state 读取棋盘。在快速连按时,可能两次方向键在 React 重新渲染之前就触发了,于是第二次读到的是一块过期的棋盘,移动就被丢掉了。修法是保留一个 ref 镜像最新的棋盘和分数,让移动处理器去读这个 ref。state 决定你看到什么;ref 驱动逻辑。改完之后,哪怕你狂按方向键也乖乖听话了。

续玩和最高分,全在本地

有两样东西存在 localStorage 里:你的最高分,以及当前棋盘。每走一步就保存棋盘;游戏结束时清掉它。加载时,如果存在一局有效的未完成游戏,我就把它恢复出来,并显示一个小小的"继续"提示——这跟我在数独单词搜索里用的是同一套模式。什么都不上传;你的游戏是你的,留在你的设备上。

试一试

你可以在这里玩 2048——键盘或滑动操作,页面加载过一次后就能离线玩。如果你喜欢这类东西,数独生成器背后还有个更有意思的算法。

常见问题

2048 难写吗? 核心小得出奇——一个"滑动并合并一条线"的函数,复用到全部四个方向,外加生成一个方块和判断游戏结束。打磨的部分(动画、触摸操作、保存进度)才是大部分时间花掉的地方。

合并规则是怎么工作的? 当两个数字相同的方块被推到一起时,它们会合并成一个数值翻倍的方块——而且每个方块每次移动只能合并一次,所以一行四个 2 会变成两个 4,而不是一个 8。

四个方向是怎么处理的? 你只需要实现一个方向。"右"就是把行翻转后的"左";"上"和"下"则是把同样的逻辑套用到列上而不是行上。

ToolKoala 的 2048 会保存我的游戏吗? 会——你的最高分和当前棋盘都存在你浏览器的本地存储里,所以你可以关掉标签页,之后再接着玩。它从不离开你的设备。

— Milo 🐨