在这里插入图片描述
在这里插入图片描述

实例:2048 小游戏(Game 2048)|技术:二维数组棋盘、滑动合并算法、随机生成、胜负判定

一、为什么选 2048 收官

1.1 经典算法游戏

2048 是风靡全球的数字合并游戏:4×4 棋盘,滑动合并相同数字,目标合成 2048。它比贪吃蛇(34)的算法更有趣:

  1. 二维数组棋盘——状态是 4×4 矩阵,比贪吃蛇的坐标队列更"结构化";
  2. 滑动合并算法——四个方向的"移动 + 合并"是核心算法题(面试常考);
  3. 随机性——每次滑动后随机生成新方块,游戏不确定性;
  4. 胜负判定——2048 胜利、无路可走失败。

2048 是算法 + 绘制 + 交互的完美收官:把本系列所有 Canvas 技巧(网格绘制、触摸滑动、状态管理)集成到一个经典游戏中。

1.2 游戏规则

规则 说明
棋盘 4×4,初始两个随机方块(2 或 4)
滑动 上下左右滑动,所有方块朝该方向移动
合并 相邻相同数字滑动中合并(每个方块每轮只合并一次)
生成 每次有效滑动后,随机空位生成 2(90%)或 4(10%)
计分 合并得分 = 合并后的数值
胜负 出现 2048 胜利;无空位且无法合并失败

二、棋盘数据模型

/** 棋盘 4×4 */
const SIZE = 4;
/** 方块颜色映射 */
const TILE_COLORS: Record<number, string> = {
  2: '#EEE4DA', 4: '#EDE0C8', 8: '#F2B179', 16: '#F59563',
  32: '#F67C5F', 64: '#F65E3B', 128: '#EDCF72', 256: '#EDCC61',
  512: '#EDC850', 1024: '#EDC53F', 2048: '#EDC22E',
};

private board: number[][] = [];

二维数组棋盘board[r][c] 存第 r 行第 c 列的数值(0 = 空)。颜色映射表:数值 → 背景色(经典 2048 配色),数字越大颜色越深——颜色与数值语义绑定

三、滑动合并算法 ★核心算法

3.1 单行压缩合并

private slideLine(line: number[]): { newLine: number[]; gained: number } {
  // ① 去掉 0
  const nums = line.filter((v) => v !== 0);
  const result: number[] = [];
  let gained = 0;
  // ② 相邻相同合并(每个元素最多合并一次)
  for (let i = 0; i < nums.length; i++) {
    if (i + 1 < nums.length && nums[i] === nums[i + 1]) {
      result.push(nums[i] * 2);
      gained += nums[i] * 2;
      i++;   // 跳过被合并的
    } else {
      result.push(nums[i]);
    }
  }
  // ③ 补零到 SIZE
  while (result.length < SIZE) {
    result.push(0);
  }
  return { newLine: result, gained: gained };
}

核心算法"去零 → 合并 → 补零"三步

① 去零filter(v => v !== 0)——把非零元素紧凑排列。如 [2, 0, 2, 4][2, 2, 4]

② 合并:遍历紧凑数组,nums[i] === nums[i+1] 时合并(×2 入结果 + 计分),i++ 跳过被合并元素——"每个元素每轮只合并一次"的关键(否则 [2,2,2,2] 会连乘成 16,而正确结果应是 [4,4,0,0])。如 [2,2,4][4,4]

③ 补零:结果不足 SIZE 补 0——[4,4][4,4,0,0](左对齐压缩)。

返回值:新行 + 本行得分(合并值之和)。

3.2 四方向滑动

private move(direction: number): boolean {
  let moved = false;
  let gained = 0;
  const newBoard: number[][] = [[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0]];
  for (let i = 0; i < SIZE; i++) {
    // 取出当前行/列(方向决定正序/倒序)
    let line: number[] = [];
    for (let j = 0; j < SIZE; j++) {
      if (direction === 0) line.push(this.board[i][j]);                // 左:行正序
      else if (direction === 1) line.push(this.board[i][SIZE-1-j]);    // 右:行倒序
      else if (direction === 2) line.push(this.board[j][i]);           // 上:列正序
      else line.push(this.board[SIZE-1-j][i]);                         // 下:列倒序
    }
    const r = this.slideLine(line);
    gained += r.gained;
    // 写回(方向决定写回位置)
    for (let j = 0; j < SIZE; j++) {
      if (direction === 0) newBoard[i][j] = r.newLine[j];
      else if (direction === 1) newBoard[i][SIZE-1-j] = r.newLine[j];
      else if (direction === 2) newBoard[j][i] = r.newLine[j];
      else newBoard[SIZE-1-j][i] = r.newLine[j];
    }
  }
  // 比较是否变化(无变化 = 无效滑动,不生成新方块)
  for (let r = 0; r < SIZE; r++) {
    for (let c = 0; c < SIZE; c++) {
      if (newBoard[r][c] !== this.board[r][c]) {
        moved = true;
      }
    }
  }
  if (moved) {
    this.board = newBoard;
    this.score += gained;
    this.spawnTile();      // 有效滑动才生成新方块
    this.checkEnd();
  }
  this.draw();
  return moved;
}

四方向统一处理的技巧——“正序/倒序 + 写回”

方向 取行/列 取序 写回
正序 [0…3] 正序
倒序 [3…0] 倒序
正序 正序
倒序 倒序

核心洞察:所有方向的滑动都可以归结为"把一条线朝’起点’方向压缩"。向左 = 行正序压缩;向右 = 行倒序压缩后写回倒序(等效于"向右压缩");上下同理用列。方向只是取序与写回的镜像,合并算法只写一遍(slideLine 永远压缩到左侧)。

有效性判定:新棋盘与旧棋盘逐格比较,有任何变化才算有效滑动——无效滑动(方块已顶满无法移动)不生成新方块、不计分。这是 2048 规则的关键细节。

四、随机生成

private spawnTile(): void {
  const empty: Array<{ r: number; c: number }> = [];
  for (let r = 0; r < SIZE; r++) {
    for (let c = 0; c < SIZE; c++) {
      if (this.board[r][c] === 0) {
        empty.push({ r: r, c: c });
      }
    }
  }
  if (empty.length === 0) {
    return;
  }
  const cell = empty[Math.floor(Math.random() * empty.length)];
  this.board[cell.r][cell.c] = Math.random() < 0.9 ? 2 : 4;
}

随机空位:收集所有空位坐标,随机选一个;数值 90% 概率 2、10% 概率 4(经典概率)。无空位直接返回(此时游戏应已结束,防御)。

五、胜负判定

private canMove(): boolean {
  for (let r = 0; r < SIZE; r++) {
    for (let c = 0; c < SIZE; c++) {
      if (this.board[r][c] === 0) return true;              // 有空位
      if (c + 1 < SIZE && this.board[r][c] === this.board[r][c+1]) return true;  // 横向可合并
      if (r + 1 < SIZE && this.board[r][c] === this.board[r+1][c]) return true;  // 纵向可合并
    }
  }
  return false;
}

private checkEnd(): void {
  // 胜利:任一格 = 2048
  for (let r = 0; r < SIZE; r++) {
    for (let c = 0; c < SIZE; c++) {
      if (this.board[r][c] === 2048) {
        this.statusText = '🎉 2048!你赢了!';
        return;
      }
    }
  }
  // 失败:无空位且无相邻相同
  if (!this.canMove()) {
    this.statusText = `💀 游戏结束,得分 ${this.score}`;
  }
}

死局判定(canMove):① 有空位 → 还能走;② 横向相邻相同 → 能合并;③ 纵向相邻相同 → 能合并。三者皆无 = 死局。"是否还有合法操作"的全局判定——遍历棋盘检查三种可能性。

胜负优先级:先查胜利(2048 出现即赢),再查失败(死局)。2048 达成后仍可继续玩(本实例胜利即停提示,不强制结束——经典版本可选继续)。

六、技术要点对照表

技术点 实现方式 生产价值
棋盘模型 二维数组 board[r][c] 结构化状态
去零合并补零 filter + 相邻合并 + 补零 单行算法
只合并一次 i++ 跳过 规则正确性
四方向统一 正序/倒序 + 镜像写回 算法只写一遍
有效性判定 新旧棋盘比较 无效滑动不生成
随机生成 空位数组 + 概率 2/4 不确定性
死局判定 空位/横/纵三查 全局判断
颜色映射 Record 数值→颜色 语义配色

七、文章小结

本篇完成了 2048 的数据与算法层:二维数组棋盘"去零 → 合并 → 补零"单行算法(含"只合并一次"的 i++ 关键)、四方向统一的镜像技巧有效性判定随机生成与胜负判定。这套算法是 2048 的灵魂——尤其"四方向统一到 slideLine"的设计,把 4 个方向的逻辑收敛为 1 个函数 + 取序写回,是"算法复用"的教科书示范。

下一篇《页面 UI 与滑动交互》将搭建:棋盘画布(aspectRatio 正方形)、方块渲染(颜色/字号分级)、触摸滑动判定、得分显示。


八、深度扩展

1. 动画过渡

真实 2048 有滑动动画(方块平滑移动 + 合并缩放)。Canvas 实现:记录"上一帧位置 + 当前帧位置",帧循环插值。插值动画提升质感,本实例聚焦算法教学,动画留作扩展。

2. 撤销(悔棋)

保存历史棋盘栈(每次移动前 push),撤销 = pop 恢复。历史栈是状态回退的通用结构(26 快递的撤销同款思路)。

3. 最高分持久化

用 preferences 存最高分(34 贪吃蛇已演示)。跨局记录激励重玩。

4. 大棋盘(5×5/6×6)

SIZE 参数化——slideLine 补零到 SIZE、绘制循环按 SIZE,改常量即扩展。算法与尺寸解耦

5. FAQ

Q1:为什么合并时 i++ 跳过被合并的元素?
A:保证"每个元素每轮最多参与一次合并"。[2,2,2,2] 左滑:i=0 合并 2+2=4,i++ 到 2(跳过第二个 2),i=2 再合并 4——结果 [4,4,0,0]。若不清跳,会连乘成 16。一次合并的规则靠指针跳跃实现

Q2:为什么右滑用"倒序取 + 倒序写回"?
A:slideLine 固定向左压缩。向右压缩 = 先把行倒序(让"最右"变"最左"),压缩后写回倒序位置——镜像变换让方向归一

Q3:无效滑动真的不生成方块吗?
A:是。moved 标志判定——方块已顶满无法移动时滑动无变化,不生成新方块(避免"白嫖"空位)。有效性判定防规则漏洞

Q4:死局判定会不会漏判?
A:不会——空位、横向相邻同、纵向相邻同三查覆盖全部合法操作。相邻相同一定能合并(滑动使它们相邻)。判定完备性可证明。

Q5:2048 胜利后还能继续吗?
A:本实例提示胜利但棋盘可继续滑(checkEnd 只设文案不锁棋盘)。经典版本可加"继续/结束"选择。胜利不是硬终止


九、下篇预告

下一篇《页面 UI 与滑动交互》将完成:正方形棋盘画布(aspectRatio(1))、方块渲染(颜色映射 + 字号分级)、触摸滑动(主导轴判定)、得分与状态提示、新游戏按钮。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐