news 2026/10/30 8:40:26

纯JS实现2048小游戏:从核心算法到二次开发实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
纯JS实现2048小游戏:从核心算法到二次开发实战

简介:这是一份面向初级Web工程师的2048网页小游戏完整源码包,以纯JavaScript实现,不依赖任何外部框架,适合作为练手项目理解前端核心机制。代码覆盖HTML结构搭建、CSS布局与动画、JavaScript基础语法、DOM操作、二维数组与矩阵运算、移动合并算法、事件驱动编程及游戏状态管理等知识点,能帮助初学者把零散语法串成可运行的小项目。资源共3个文件,包含1个js脚本、1个jpg图片和1个docx说明文档,压缩包约830KB,体积轻量便于本地运行与逐行阅读。目前已有417人学习下载,可作为课程作业、自学练习或面试前的前端基础复盘素材,读者能从中获得可直接运行的完整代码、界面素材与配套说明,并借此梳理游戏逻辑与调试思路。

1. 从一次“手痒”说起:为什么我建议你亲手跑一遍这个 2048

前阵子帮一个刚转前端的朋友看简历,项目栏里清一色写着“仿某某商城”“后台管理系统”,问起来却连一个完整的状态管理都讲不清。我随手丢给他一个网页版 2048 的源码包,让他先跑起来再说。结果他折腾半小时后跑来问:为什么键盘按下去方块不动?我一看,焦点没落在游戏容器上。就这么个小坑,恰恰暴露了很多初级 web 工程师的通病——能写页面,但没真正理解“事件、状态、渲染”这条闭环。

这个纯 JS 开发的 2048 小游戏,正好是补这一课的好材料。它没有框架、没有构建工具、没有 npm 依赖,打开 HTML 就能玩,代码量控制在一个人一下午能通读的范围内。但它麻雀虽小,五脏俱全:二维数组表示棋盘、方向键监听、合并算法、随机生成新块、分数统计、游戏结束判定,一个都不少。对初级 web 工程师来说,它比任何教程都实在——你能看到每一行代码是怎么把“按一下方向键”变成“屏幕上数字跳动”的。这份源码还带了完整图片资源,意味着你连样式和素材都不用自己找,clone 下来就能改。接下来我按“先跑通、再拆解、最后动手改”的顺序,把这个项目从头到尾讲一遍。

2. 项目结构与技术选型:一个不依赖框架的纯前端闭环

2.1 目录里都有什么

拿到源码包,先别急着打开浏览器。我习惯先tree一下看结构,这个项目的组织方式非常“教科书式”的轻量:

2048-game/ ├── index.html # 唯一入口页面 ├── css/ │ └── style.css # 棋盘、方块、动画样式 ├── js/ │ ├── main.js # 入口:初始化、事件绑定 │ ├── game.js # 核心逻辑:移动、合并、判定 │ └── render.js # 渲染层:把数据映射到 DOM └── images/ ├── bg.png # 背景图 └── tile-*.png # 各数字方块贴图(可选)

这个分层不是摆设。game.js只管数据,render.js只管画,main.js负责把两者接起来。你改逻辑不用碰样式,换皮肤不用动算法。很多新手写小游戏喜欢把所有代码塞进一个<script>里,跑是能跑,但改起来就是灾难。这个项目的分层意识,本身就是值得学的工程习惯。

2.2 为什么不用框架

有人会问:都 2024 年了,为什么不用 Vue 或 React?我的看法是,2048 这种规模的项目,用框架反而是负担。它的状态极其简单——就是一个 4×4 的二维数组加一个分数,没有路由、没有异步请求、没有复杂组件通信。用原生 JS,你能清楚看到“数据变了 → 手动调 render”这条链路,而不是被框架的响应式系统遮住。对初级工程师来说,先理解手动渲染,再去用框架的自动渲染,认知顺序才是对的。这也是我推荐拿它练手的原因:它逼你直面 DOM 操作和事件模型,这两样东西是框架底下永远绕不开的地基。

2.3 运行环境要求

这个项目对环境的唯一要求就是——一个现代浏览器。不需要 Node,不需要本地服务器(当然起一个更规范)。如果你装了 Python,在项目根目录执行python -m http.server 8000,然后访问localhost:8000就行。直接双击index.html也能跑,但有些浏览器对file://协议下的图片加载和本地存储有限制,起个静态服务器更稳妥。这一点后面避坑章节还会细说。

3. 核心逻辑拆解:一个方向键按下后发生了什么

3.1 棋盘的数据表示

整个游戏的“真相”只有一个变量:一个 4×4 的二维数组。0 表示空格,其余数字就是方块值。所有视觉呈现都是这个数组的投影。

// game.js const SIZE = 4; // 初始化一个全 0 的棋盘 function createBoard() { return Array.from({ length: SIZE }, () => Array(SIZE).fill(0)); } // 在随机空格生成一个 2 或 4(90% 概率是 2) function addRandomTile(board) { const empty = []; for (let r = 0; r < SIZE; r++) { for (let c = 0; c < SIZE; c++) { if (board[r][c] === 0) empty.push({ r, c }); } } if (empty.length === 0) return board; const { r, c } = empty[Math.floor(Math.random() * empty.length)]; board[r][c] = Math.random() < 0.9 ? 2 : 4; return board; }

createBoard用Array.from而不是new Array(4).fill([]),是因为后者会让四行共享同一个引用,改一行全变——这是新手极容易翻车的地方。addRandomTile先收集所有空格坐标,再随机挑一个,保证不会覆盖已有方块。2 和 4 的 9:1 概率是 2048 的经典设定,你可以按需调整这个比例来改变难度。

3.2 单行合并算法:整个游戏的心脏

向左移动时,每一行独立处理:先把非零数字挤到左边,再从左往右扫描,相邻且相等就合并、分数累加,合并后的位置标记为已处理,防止一次移动里连续合并(比如2 2 2 2应该变成4 4而不是8)。

// 处理单行向左合并,返回新行和本次得分 function slideLeft(row) { // 1. 去掉 0,压缩到左侧 let tiles = row.filter(v => v !== 0); let score = 0; // 2. 从左往右扫描合并 for (let i = 0; i < tiles.length - 1; i++) { if (tiles[i] === tiles[i + 1]) { tiles[i] *= 2; score += tiles[i]; tiles[i + 1] = 0; i++; // 跳过下一个,防止连锁合并 } } // 3. 再次压缩,补 0 到长度 4 tiles = tiles.filter(v => v !== 0); while (tiles.length < SIZE) tiles.push(0); return { row: tiles, score }; }

这段代码有三个关键点。第一,filter(v => v !== 0)先压缩,把[0,2,0,2]变成[2,2],这样合并判断才准确。第二,合并后i++再配合循环的i++,等于跳两格,避免[2,2,4]里刚合并出的 4 又和后面的 4 合并。第三,最后再压缩一次并补零,保证返回的行长度恒为 4。这个函数是纯函数,输入一行输出一行,不碰全局状态,测试起来非常方便——你可以单独在控制台调它验证各种边界情况。

3.3 四个方向的统一处理

向右、向上、向下不需要写四套逻辑。常见做法是:向右就把行反转,用slideLeft处理完再反转回来;向上向下则先做矩阵转置,把列变成行,处理完再转置回去。

// 矩阵转置:行列互换 function transpose(board) { return board[0].map((_, c) => board.map(row => row[c])); } // 统一入口:direction 为 'left' | 'right' | 'up' | 'down' function move(board, direction) { let working = board.map(row => [...row]); // 深拷贝,避免污染原棋盘 let totalScore = 0; let moved = false; if (direction === 'up' || direction === 'down') { working = transpose(working); } for (let i = 0; i < SIZE; i++) { let row = working[i]; if (direction === 'right' || direction === 'down') row = row.reverse(); const result = slideLeft(row); if (direction === 'right' || direction === 'down') result.row.reverse(); // 判断是否真的发生了变化 if (result.row.some((v, idx) => v !== working[i][idx])) moved = true; working[i] = result.row; totalScore += result.score; } if (direction === 'up' || direction === 'down') { working = transpose(working); } return { board: working, score: totalScore, moved }; }

moved这个标志很重要:只有棋盘真的变了,才应该生成新方块、才需要重绘。如果玩家按了一个无效方向(比如所有方块都贴左边了还按左),棋盘不变,就不该冒出新数字。working用map(row => [...row])做深拷贝,是因为后面会原地修改行数据,不拷贝会直接改掉传入的棋盘,导致“撤销”之类的功能没法做。转置加反转这套组合拳,把四个方向收敛成一个slideLeft,代码量直接砍掉四分之三,这是我觉得这个项目最值得学的设计。

3.4 游戏结束判定

结束条件有两个:棋盘满了,且四个方向都动不了。只判断“满了”是不够的,因为满盘但还有相邻相等的情况仍可继续。

function isGameOver(board) { // 还有空格就没结束 for (let r = 0; r < SIZE; r++) for (let c = 0; c < SIZE; c++) if (board[r][c] === 0) return false; // 检查水平和垂直方向是否有相邻相等 for (let r = 0; r < SIZE; r++) { for (let c = 0; c < SIZE; c++) { const v = board[r][c]; if (c < SIZE - 1 && v === board[r][c + 1]) return false; if (r < SIZE - 1 && v === board[r + 1][c]) return false; } } return true; }

先扫空格,有空格直接返回 false,省掉后续判断。然后只检查右邻和下邻,因为左邻和上邻在遍历过程中已经被前一个格子的右/下判断覆盖了,不用重复。这个细节能看出作者是认真想过遍历顺序的。

4. 渲染与交互:把数组变成会动的方块

4.1 DOM 结构与渲染策略

渲染层有两种常见做法:一是每次全量重建棋盘 DOM,二是复用格子只更新数字和样式。这个项目用的是前者——简单直接,4×4 一共 16 个格子,重建开销可以忽略。

// render.js function render(board) { const container = document.getElementById('board'); container.innerHTML = ''; // 清空重绘 board.forEach((row, r) => { row.forEach((value, c) => { const cell = document.createElement('div'); cell.className = 'cell'; if (value > 0) { cell.classList.add(`tile-${value > 2048 ? 'super' : value}`); cell.textContent = value; } container.appendChild(cell); }); }); }

tile-${value}这个类名约定,让 CSS 可以针对每个数字单独配色,比如tile-2是浅灰、tile-2048是金色。超过 2048 的值统一走tile-super,避免类名爆炸。全量重绘的代价是丢失 CSS 过渡动画——因为 DOM 是新建的,浏览器不会对“新元素”做 transition。如果你想要方块滑动的动画,就得改成复用格子、只改textContent和class的方案,这是二次开发时第一个要动的地方,后面会细说。

4.2 键盘事件绑定

交互入口在main.js,核心是监听keydown,把按键映射成方向,调move,再决定是否生成新块和重绘。

// main.js const KEY_MAP = { ArrowLeft: 'left', ArrowRight: 'right', ArrowUp: 'up', ArrowDown: 'down', a: 'left', d: 'right', w: 'up', s: 'down' // 兼容 WASD }; let board = createBoard(); let score = 0; addRandomTile(board); addRandomTile(board); render(board); document.addEventListener('keydown', (e) => { const dir = KEY_MAP[e.key]; if (!dir) return; e.preventDefault(); // 阻止方向键滚动页面 const result = move(board, dir); if (!result.moved) return; // 无效移动,什么都不做 board = result.board; score += result.score; addRandomTile(board); render(board); updateScore(score); if (isGameOver(board)) { showGameOver(); } });

e.preventDefault()这行不能省,否则按方向键时整个页面会跟着滚动,体验很差。KEY_MAP同时支持方向键和 WASD,照顾不同习惯的玩家。注意if (!result.moved) return的位置——在生成新块之前就返回,保证无效操作不会凭空多出一个方块。这个顺序如果写反,就会出现“按了没反应但棋盘上多了个 2”的诡异现象,是新手常见的逻辑漏洞。

4.3 分数与最高分持久化

分数显示很简单,一个updateScore更新 DOM 文本即可。最高分用localStorage存,刷新页面不丢。

function updateScore(score) { document.getElementById('score').textContent = score; const best = Number(localStorage.getItem('best2048') || 0); if (score > best) { localStorage.setItem('best2048', score); document.getElementById('best').textContent = score; } }

localStorage存的是字符串,读出来要Number()转一下,否则比较时会出现"100" > 99这种字符串和数字混比的坑(虽然 JS 会隐式转换,但显式转更稳)。键名加个best2048前缀,避免和同域下其他页面的存储冲突。这套持久化思路可以原样搬到任何小游戏里,是很好的练手模板。

5. 避坑与排查:我踩过的五个坑

5.1 双击打开后图片不显示

现象:直接双击index.html,背景图和方块贴图全是裂的。原因:部分浏览器在file://协议下对相对路径资源加载有安全限制,尤其是图片和字体。解决:起一个本地静态服务器,python -m http.server 8000或npx serve都行,用http://localhost:8000访问。这个坑几乎每个新手都会踩一次,记住“涉及资源加载就用服务器”这条经验。

5.2 按方向键页面跟着滚

现象:游戏能玩,但每按一次方向键,整个页面就上下跳一下。原因:方向键的默认行为是滚动页面,keydown里没阻止。解决:在事件处理里加e.preventDefault(),并且要放在确认是方向键之后、执行移动之前。注意别加在全局所有按键上,否则输入框里打字都会被拦。

5.3 一次移动合并出“8”而不是“4 4”

现象:一行2 2 2 2向左移,期望得到4 4,实际得到8。原因:合并循环里没有跳过已合并的元素,导致新生成的 4 又和下一个 4 合并。解决:合并成功后i++额外跳一格,或者用一个merged标记数组记录哪些位置本回合已合并。这是 2048 算法最经典的 bug,建议单独写几个测试用例把[2,2,2,2]、[2,2,4]、[4,4,8,8]都跑一遍。

5.4 无效移动也生成新方块

现象:所有方块都贴左边了,再按左键,棋盘没变但凭空多了个 2。原因:move返回后没判断moved就直接addRandomTile。解决:move内部逐行比较新旧行是否一致,汇总成moved标志,主流程里if (!result.moved) return。这个判断要放在生成新块和重绘之前,顺序不能错。

5.5 刷新后最高分归零

现象:玩到 5000 分,刷新页面最高分变回 0。原因:localStorage的 key 拼写不一致,或者读取时没做空值兜底。解决:统一用一个常量存 key 名,读取时Number(localStorage.getItem(KEY) || 0),写入前先比较。另外注意隐私模式下localStorage可能抛异常,稳妥的做法是包一层try/catch,失败就退化成内存变量,不影响游戏本身。

6. 二次开发:从能玩到好玩,我一般这么改

跑通之后,这个项目真正的价值才显现——它是一个可以随便折腾的沙盒。我通常按三个方向动手,每个方向都能练到不同的前端能力。

第一个方向是加动画。前面说过全量重绘会丢过渡,改法是复用 16 个格子,初始化时建好,之后只更新textContent和className。给.cell加transition: transform 0.1s, background 0.1s,数字变化时就有缩放和变色效果。更进一步可以做“滑动”动画:记录每个方块移动前后的坐标,用transform: translate做位移,这需要把数据结构从“二维数组”升级成“带 id 的方块对象列表”,是个不小的重构,但做完你对“数据驱动视图”的理解会上一个台阶。

第二个方向是加撤销。因为move里已经做了深拷贝,实现撤销很简单:每次有效移动前把board和score压进一个历史栈,撤销时弹出恢复。注意栈深度要限制(比如 20 步),否则长时间玩内存会涨。这个功能能让你体会“状态快照”的思路,和很多编辑器、绘图工具的撤销机制是相通的。

第三个方向是换交互。键盘玩腻了可以加触摸滑动,监听touchstart和touchend,算两个点的位移向量,取绝对值大的方向作为移动方向。移动端还要处理touchmove的preventDefault防止页面滚动。加完之后这个游戏就能在手机上玩了,虽然布局要配合媒体查询调一下格子尺寸。

改造方向主要改动文件难度练到的能力
过渡动画render.js / style.css低CSS transition、DOM 复用
滑动动画game.js / render.js中数据结构重构、坐标计算
撤销功能main.js / game.js低状态快照、栈结构
触摸支持main.js中触摸事件、手势识别

改完记得做回归验证:每次改动后,手动跑一遍“四个方向各按几次 + 触发一次合并 + 玩到游戏结束”,确认没有引入新 bug。有条件的话把slideLeft和isGameOver抽出来写几个断言,用浏览器控制台或简单的测试脚本跑,比肉眼盯着屏幕靠谱得多。

说个我自己的教训。早些年我改这类小游戏,总喜欢一上来就重构目录、上构建工具,结果半天时间全花在配环境上,游戏本身一行没动。后来我给自己定了条规矩:先让原版跑起来、玩两把、确认每个功能都正常,再动手改,而且一次只改一个点,改完立刻验证。这个习惯让我少走了很多弯路。这份 2048 源码不大,但正好够你把“读代码 → 跑起来 → 改一处 → 验证”这个循环完整走一遍,走完你对纯前端项目的掌控感会完全不一样。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/30 8:39:24

早期超越版微积分第15版:教材编排与学习路径解析

简介&#xff1a;《Thomas Calculus, Early Transcendentals》第十五版是面向理工科本科生、考研备考者与微积分自学者的一份英文经典教材资源&#xff0c;2023年由Pearson出版&#xff0c;属于高清非扫描电子版&#xff0c;可直接检索与复制公式文本&#xff0c;适合作为课堂学…

作者头像 李华
网站建设 2026/10/30 8:38:15

分布式数据库核心架构与性能优化实战

1. 分布式数据库系统概述 分布式数据库系统&#xff08;Distributed Database System&#xff09;是现代企业级应用的核心基础设施之一。作为一名从业十余年的系统架构师&#xff0c;我见证了分布式数据库从学术概念到行业标配的演进过程。简单来说&#xff0c;它是由多个物理上…

作者头像 李华
网站建设 2026/10/30 8:37:48

Java纯Java读取Access数据库:ucanaccess 4.0.4实战指南

简介&#xff1a;本资源是面向Java开发者与数据库应用工程师的Access数据库直连解决方案&#xff0c;专为替代受限的Access_JDBC30.jar而设计。UCanAccess-4.0.4.jar作为开源免费核心组件&#xff0c;支持纯JDBC方式直接读写.mdb文件&#xff0c;彻底规避JDBC-ODBC桥接依赖&…

作者头像 李华
网站建设 2026/10/30 8:36:33

高通WiFi驱动编程指南:AP侧WLAN驱动架构与调试实战

简介&#xff1a;《高通WiFi驱动编程指南》是一份面向无线网络开发者的技术参考文档&#xff0c;聚焦高通WLAN驱动在接入点&#xff08;AP&#xff09;设备上的安装、配置、优化与故障排查&#xff0c;适合具备一定嵌入式与网络基础、需要深入理解驱动结构的中高级开发者查阅。…

作者头像 李华
网站建设 2026/10/30 8:35:43

从CPU指令到指针栈:程序员必知的底层硬核知识

简介&#xff1a;《程序员必知的硬核知识大全》面向希望补齐计算机底层认知的程序员、面试备战者与进阶学习者&#xff0c;围绕CPU组成与主频、RAM分类、二/八/十/十六进制转换、操作系统与API、BIOS及MBR/GRUB引导流程、汇编语言、系统调用与硬件交互、调用约定、DMA、RLE与DL…

作者头像 李华
网站建设 2026/10/30 8:35:43

程序员必知底层硬核知识:CPU、内存、汇编与函数调用

简介&#xff1a;这份《程序员必知的硬核知识大全》面向希望补齐计算机底层基础的程序员与计算机专业学习者&#xff0c;围绕CPU、内存管理、进制转换、操作系统原理、BIOS与引导过程、汇编语言及软硬件交互等主题展开&#xff0c;帮助读者建立从硬件到应用的系统认知。资源包仅…

作者头像 李华