用原生 HTML+CSS+JS 写一个 DOM 版 2048:方向键滑动、相同数字合并、随机生成 2/4、游戏结束判断。不依赖任何框架,浏览器打开即玩。

1. 页面结构

用 4×4 的 <div> 网格做棋盘,每个格子一个 <div>,数字和背景色都通过 JS 更新:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8"/>
<title>2048</title>
<style>
  body{font-family:sans-serif;text-align:center;background:#222;color:#eee}
  #board{display:grid;grid-template-columns:repeat(4,100px);gap:8px;
         width:424px;margin:20px auto;background:#bbada0;padding:8px;border-radius:8px}
  .cell{width:100px;height:100px;border-radius:6px;background:#cdc1b4;
        display:flex;align-items:center;justify-content:center;
        font-size:34px;font-weight:bold;color:#776e65}
</style>
</head>
<body>
<h1>2048</h1>
<div id="board"></div>
<div id="info"></div>
<script>
// 逻辑写这里
</script>
</body>
</html>

2. 滑动合并

把一行抽出来:去 0 → 相邻相同合并 → 补 0。四个方向用"反转/转置"复用同一个函数:

function slide(line) {
  let a = line.filter(v => v !== 0), b = [];
  for (let i = 0; i < a.length; i++) {
    if (i + 1 < a.length && a[i] === a[i + 1]) {
      b.push(a[i] * 2); score += a[i] * 2; i++;
    } else b.push(a[i]);
  }
  while (b.length < 4) b.push(0);
  return b;
}

function moveGrid(dir) {   // 0左 1右 2上 3下
  for (let i = 0; i < 4; i++) {
    let line = dir < 2 ? grid[i].slice() : grid.map(r => r[i]);
    if (dir === 1 || dir === 3) line.reverse();
    line = slide(line);
    if (dir === 1 || dir === 3) line.reverse();
    for (let j = 0; j < 4; j++) {
      if (dir < 2) grid[i][j] = line[j];
      else grid[j][i] = line[j];
    }
  }
  spawn(); render();
}

3. 完整代码

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8"/>
<title>2048</title>
<style>
  body{font-family:sans-serif;text-align:center;background:#222;color:#eee}
  #board{display:grid;grid-template-columns:repeat(4,100px);gap:8px;
         width:424px;margin:20px auto;background:#bbada0;padding:8px;border-radius:8px}
  .cell{width:100px;height:100px;border-radius:6px;background:#cdc1b4;
        display:flex;align-items:center;justify-content:center;
        font-size:34px;font-weight:bold;color:#776e65}
</style>
</head>
<body>
<h1>2048 <small>方向键移动</small></h1>
<div id="board"></div>
<div id="info"></div>
<script>
const COLORS = {2:'#eee4da',4:'#ede0c8',8:'#f2b179',16:'#f59563',32:'#f67c5f',
  64:'#f65e3b',128:'#edcf72',256:'#edcc61',512:'#edc850',1024:'#edc53f',2048:'#edc22e'};
let grid = Array.from({length:4}, () => Array(4).fill(0));
let score = 0;
const board = document.getElementById('board');
for (let i = 0; i < 16; i++) {
  const d = document.createElement('div');
  d.className = 'cell';
  board.appendChild(d);
}

function slide(line) {
  let a = line.filter(v => v !== 0), b = [];
  for (let i = 0; i < a.length; i++) {
    if (i + 1 < a.length && a[i] === a[i + 1]) { b.push(a[i] * 2); score += a[i] * 2; i++; }
    else b.push(a[i]);
  }
  while (b.length < 4) b.push(0);
  return b;
}

function moveGrid(dir) {
  for (let i = 0; i < 4; i++) {
    let line = dir < 2 ? grid[i].slice() : grid.map(r => r[i]);
    if (dir === 1 || dir === 3) line.reverse();
    line = slide(line);
    if (dir === 1 || dir === 3) line.reverse();
    for (let j = 0; j < 4; j++) {
      if (dir < 2) grid[i][j] = line[j];
      else grid[j][i] = line[j];
    }
  }
  spawn(); render();
}

function spawn() {
  const empty = [];
  for (let r = 0; r < 4; r++) for (let c = 0; c < 4; c++)
    if (!grid[r][c]) empty.push([r, c]);
  if (!empty.length) return;
  const [r, c] = empty[Math.floor(Math.random() * empty.length)];
  grid[r][c] = Math.random() < 0.9 ? 2 : 4;
}

function canMove() {
  for (let r = 0; r < 4; r++) for (let c = 0; c < 4; c++) {
    if (!grid[r][c]) return true;
    if (c + 1 < 4 && grid[r][c] === grid[r][c + 1]) return true;
    if (r + 1 < 4 && grid[r][c] === grid[r + 1][c]) return true;
  }
  return false;
}

function render() {
  const cells = board.children;
  for (let i = 0; i < 16; i++) {
    const v = grid[Math.floor(i / 4)][i % 4];
    cells[i].textContent = v || '';
    cells[i].style.background = COLORS[v] || '#cdc1b4';
    cells[i].style.color = v > 4 ? '#fff' : '#776e65';
  }
  document.getElementById('info').textContent = '分数: ' + score;
  if (!canMove()) alert('游戏结束!分数: ' + score);
}

document.addEventListener('keydown', e => {
  const map = {ArrowLeft:0, ArrowRight:1, ArrowUp:2, ArrowDown:3};
  if (e.key in map) { e.preventDefault(); moveGrid(map[e.key]); }
});

spawn(); spawn(); render();
</script>
</body>
</html>

4. 运行与常见问题

保存为 2048.html 用浏览器打开。常见问题:①方向键滚页面——e.preventDefault();②新块每次都生成——moveGrid 后无条件 spawn,想做"有效才生成"就对比滑动前后;③手机玩不了——加触摸事件。

💡 改进方向:①触摸滑动;②撤销;③最高分存 localStorage;④动画过渡;⑤移动端自适应。