简介
一个使用原生 HTML/CSS/JavaScript 实现的 2048 小游戏,支持 3×3、4×4、5×5、6×6 四种棋盘规模,包含计分、胜利与失败判定、动态布局与多主题数字配色。
演示
功能特性
- 多棋盘:3×3 / 4×4 / 5×5 / 6×6 一键切换。
- 键盘操作:方向键 ↑ ↓ ← → 控制移动合并。
- 自适应布局:不同棋盘规模自动调整格子尺寸与字号。
- 计分与结算:合并得分,胜利 / 失败弹窗提示。
- 纯前端:无需依赖库,直接打开即可游玩。
本地运行
方式一:直接用现代浏览器打开 index.html 即可。
方式二:启一个本地静态服务器(推荐便于调试):
# 进入项目目录
cd /Users/yanqiansong/Downloads/canvas-special-master/game2048
# 使用 Python 内置静态服务器(macOS 自带 Python3)python3 -m http.server 8080
然后在浏览器访问:http://localhost:8080/
操作说明
- 使用方向键进行移动与合并:
- ← 左移,→ 右移,↑ 上移,↓ 下移。
- 右上角点击棋盘规模按钮(3×3 / 4×4 / 5×5 / 6×6)可重开并切换模式。
- 弹窗显示
You win!或GAME OVER!后,点击Try again重新开始。
目录结构
.
├─ index.html # 单文件实现:结构、样式与脚本
实现概览
核心逻辑在 index.html 中,以一个 obj 对象管理状态与行为:
gameStart():启动绑定键盘事件并初始化棋盘与分数。init():根据棋盘规模创建格子、重置数组与分数、随机生成初始方块。create(r, c):按当前规模计算并绘制每个格子的尺寸与位置。random():在空位随机生成2或4。updateView():同步 DOM,根据数字应用不同配色;判断胜利 / 失败。isGameOver():无空位且无可合并相邻数字时判负。find():按当前方向查找下一个非 0 的数字位置。dealToLeft/Right/Up/Down():各方向合并与挪动规则实现,并累计得分。moveLeft/Right/Up/Down():封装一步移动,变更即刷新并生成新块。getModel(e):点击按钮切换棋盘规模(3~6)。
胜利条件随规模调整:
- 3×3:达到 1024
- 4×4:达到 2048
- 5×5:达到 4096
- 6×6:达到 8192
自定义与拓展
- 修改胜利阈值:在
updateView()中按需调整不同规模对应的目标数。 - 调整颜色样式:在样式表中
.n2/.n4/.../.n8192定义不同数字颜色。 - 默认棋盘规模:
obj.ROW与obj.CELL初始为 4,可设为 3~6。 - UI 与交互:可添加触摸滑动支持、撤销一步、最高分本地存储等功能。
常见问题
- 打开空白 / 样式错乱:请使用现代浏览器(Chrome/Edge/Safari 最新版)。
- 键盘无反应:确保页面焦点在游戏窗口内;或检查系统拦截快捷键设置。
- 切换规模无效:需点击右上角的 3×3/4×4/5×5/6×6 按钮进行切换(会重开局)。
源码
<!DOCTYPE html>
<html>
<head>
<title> 2048-game </title>
<meta charset="utf-8" />
<style media="screen">
#game {
display: none;
position: absolute;
left: 0px;
top: 0px;
right: 0px;
bottom: 0px;
background-color: #9DA5C3;
opacity: 0.5;
z-index: 1;
}
.clear:after {
content: "";
display: table;
clear: both;
}
.left {float: left;}
.right {float: right;}
.scoreShow {
height: 50px;
text-align: center;
line-height: 50px;
}
.model {
text-decoration: none;
color: white;
background-color: #bbada0;
font-size: 36px;
border-radius: 10px;
}
.head {
width: 480px;
height: 50px;
margin: 0 auto;
font-size: 25px;
}
#gridPanel {
width: 480px;
height: 480px;
margin: 0 auto;
background-color: #bbada0;
border-radius: 10px;
position: relative;
z-index: 1;
}
.grid,
.cell {
width: 100px;
height: 100px;
border-radius: 6px;
}
.grid {
background-color: #ccc0b3;
float: left;
margin: 16px 0 0 16px;
}
.cell {
position: absolute;
font-size: 60px;
text-align: center;
line-height: 100px;
color: #fff;
}
.n2 {background-color: #eee3da}
.n4 {background-color: #ede0c8}
.n8 {background-color: #f2b179}
.n16 {background-color: #f59563}
.n32 {background-color: #f67c5f}
.n64 {background-color: #f65e3b}
.n128 {background-color: #edcf72}
.n256 {background-color: #edcc61}
.n512 {background-color: #9c0}
.n1024 {background-color: #33b5e5}
.n2048 {background-color: #09c}
.n4096 {background-color: #a6c}
.n8192 {background-color: #93c}
.n2,
.n4 {color: #776e65}
#gameover {
width: 100%;
display: none;
position: fixed;
left: 50%;
right: 50%;
top: 148px;
width: 220px;
height: 200px;
border-radius: 10px;
background-color: white;
margin-left: -110px;
text-align: center;
z-index: 5;
}
#gameover>a {
display: inline-block;
width: 170px;
height: 50px;
border-radius: 10px;
text-decoration: none;
background-color: #9F8D77;
color: white;
font-size: 36px;
}
</style>
</head>
<body>
<div id="game">
</div>
<div class="head clear">
<div class="scoreShow left">
<span>Score:</span>
<span id="score"></span>
</div>
<div class="selction right" onclick="getModel(event)">
<a href="#" class="model" value="3">3X3</a>
<a href="#" class="model" value="4">4X4</a>
<a href="#" class="model" type="button">5X5</a>
<a href="#" class="model" type="button">6X6</a>
<!-- <input type="text" id="model"> -->
<!-- <button type="button" name="button" id="set"> 设置游戏 </button> -->
</div>
</div>
<div id="gridPanel">
</div>
<div id="gameover">
<h1 id="Score"></h1>
<a href="#" id="again" onclick="obj.gameStart()">Try again</a>
</div>
<script type="text/javascript">
var arr = [];
function $(id) {return document.getElementById(id);
}
function C(cls) {return document.getElementsByClassName(cls);
}
var obj = {
ROW: 4,
CELL: 4,
r: 0,
c: 0,
f: 0, // r 行 c 列 f 查找的下一位置
keyCd: 0,
score: 0,
createEle: 0, // 是否需要创建元素
eleFragment: "", // 文档片段变量
// 游戏开始
gameStart: function() {obj.init();
document.onkeydown = function(e) { // 自动获得事件对象
switch (e.keyCode) { // 判断按键号
case 37:
obj.keyCd = 1;
obj.moveLeft();
break;
case 38:
obj.keyCd = 2;
obj.moveUp();
break;
case 39:
obj.keyCd = 1;
obj.moveRight();
break;
case 40:
obj.keyCd = 2;
obj.moveDown();
break;
}
$("score").innerHTML = obj.score; // 更新分数
}
},
// 初始化
init: function() {obj.eleFragment = document.createDocumentFragment();
for (r = 0; r < obj.ROW; r++) {arr.push([]);
for (c = 0; c < obj.CELL; c++) {arr[r][c] = 0;
if (obj.createEle == 1) {obj.create(r, c);
}
}
}
if (obj.createEle == 1) {
obj.createEle = 0;
$("gridPanel").innerHTML = ""; // 清空原有的元素
$("gridPanel").appendChild(obj.eleFragment); // 添加元素
}
obj.score = 0;
$("score").innerHTML = obj.score;
$("game").style.display = "none";
$("gameover").style.display = "none";
obj.random(); // 开始游戏随机生成两个数
obj.random();
obj.updateView();},
// 创建 div 元素,添加到 gridPanel 中
create: function(r, c) {
var grid, cell;
var increment = 14,
grWidth, grHeight, grMarginTop, grMarginLeft, ceWidth, ceHight;
grid = document.createElement("div");
cell = document.createElement("div");
grid.id = "g" + r + c;
grid.className = "grid";
cell.id = "c" + r + c;
cell.className = "cell";
if (obj.ROW == 3) {increment = 24;} else if (obj.ROW == 4) {increment = 18;}
grWidth = grHeight = ceWidth = ceHight = 66 + (6 - obj.ROW) * increment; // 优化后
grMarginTop = grMarginLeft = (480 - grWidth * obj.ROW) / (obj.ROW + 1);
grid.style.width = grWidth + "px";
grid.style.height = grHeight + "px";
grid.style.marginTop = grMarginTop + "px";
grid.style.marginLeft = grMarginLeft + "px";
cell.style.width = ceWidth + "px";
cell.style.height = ceHight + "px";
cell.style.top = grMarginTop + r * (grMarginTop + ceWidth) + "px";
cell.style.left = grMarginLeft + c * (grMarginLeft + ceHight) + "px";
cell.style.lineHeight = ceHight + "px";
cell.style.fontSize = 30 + (6 - obj.ROW) * 10 + "px";
// 优化前
/*if (obj.ROW == 3) {
grid.style.width = "140px";
grid.style.height = "140px";
grid.style.margin = "15px 0 0 15px";
cell.style.width = "140px";
cell.style.height = "140px";
cell.style.top = 15 + r * 155 + "px"; // 设置距离上一位置的高度
cell.style.left = 15 + c * 155 + "px"; // 设置离左一位置的距离
cell.style.lineHeight = "140px";
} else if (obj.ROW == 4) {
grid.style.width = "100px";
grid.style.height = "100px";
grid.style.margin = "16px 0 0 16px";
cell.style.width = "100px";
cell.style.height = "100px";
cell.style.top = 16 + r * 116 + "px";
cell.style.left = 16 + c * 116 + "px";
cell.style.lineHeight = "100px";
} else if (obj.ROW == 5) {
grid.style.width = "75px";
grid.style.height = "75px";
grid.style.margin = "17.5px 0 0 17.5px";
cell.style.width = "75px";
cell.style.height = "75px";
cell.style.top = 17.5 + r * 92.5 + "px";
cell.style.left = 17.5 + c * 92.5 + "px";
cell.style.fontSize = "40px";
cell.style.lineHeight = "75px";
} else if (obj.ROW == 6) {
grid.style.width = "66px";
grid.style.height = "66px";
grid.style.margin = "12px 0 0 12px";
cell.style.width = "66px";
cell.style.height = "66px";
cell.style.top = 12 + r * 78 + "px";
cell.style.left = 12 + c * 78 + "px";
cell.style.fontSize = "30px";
cell.style.lineHeight = "66px";
}*/
obj.eleFragment.appendChild(grid);
obj.eleFragment.appendChild(cell);
},
// 随机产生一个新的数
random: function() {while (1) {var row = Math.floor(Math.random() * obj.ROW);
var cell = Math.floor(Math.random() * obj.CELL);
if (arr[row][cell] == 0) { // 判断生成的随机数位置为 0 才随机生成 2 或 4
arr[row][cell] = (Math.random() > 0.5) ? 4 : 2;
break;
}
}
// var row = Math.floor(Math.random() * 4);
// var cell = Math.floor(Math.random() * 4);
// if (arr[row][cell] == 0) { // 判断生成的随机数位置为 0 才随机生成 2 或 4
// arr[row][cell] = (Math.random() > 0.5) ? 4 : 2;
// return;
// }
// obj.random();// 递归影响执行效率},
// 更新页面
updateView: function() {
var win = 0;
for (r = 0; r < obj.ROW; r++) {for (c = 0; c < obj.CELL; c++) {if (arr[r][c] == 0) { // 值为 0 的不显示
$("c" + r + c).innerHTML = ""; // 0 不显示
$("c" + r + c).className = "cell" // 清除样式
} else {$("c" + r + c).innerHTML = arr[r][c];
$("c" + r + c).className = "cell n" + arr[r][c]; // 添加不同数字的颜色
if (obj.ROW == 3 && arr[r][c] == 1024) {win = 1;} else if (obj.ROW == 4 && arr[r][c] == 2048) {win = 1;} else if (obj.ROW == 5 && arr[r][c] == 4096) {win = 1;} else if (obj.ROW == 6 && arr[r][c] == 8192) {win = 1;}
}
}
}
if (win == 1) { // 通关
$("game").style.display = "block";
$("gameover").style.display = "block";
$("Score").innerHTML = "You win!<br>Score:" + obj.score;
}
if (obj.isGameOver()) { // 游戏失败
$("game").style.display = "block";
$("gameover").style.display = "block";
$("Score").innerHTML = "GAME OVER!<br>Score:" + obj.score;
console.log("gameover");
}
},
// 游戏失败
isGameOver: function() {for (r = 0; r < obj.ROW; r++) {for (c = 0; c < obj.CELL; c++) {if (arr[r][c] == 0) { // 有 0 还不是 gameover
return false;
} else if (c != obj.CELL - 1 && arr[r][c] == arr[r][c + 1]) { // 左往右 前一个和下一个不相等
return false;
} else if (r != obj.ROW - 1 && arr[r][c] == arr[r + 1][c]) { // 上往下 上一个和下一个不相等
return false;
}
}
}
return true;
},
// 查找下一个不为 0 的数值的位置
find: function(r, c, start, condition, direction) {if (obj.keyCd == 2) { // 上下按键
if (direction == 1) { // 向上按键 f++
for (var f = start; f < condition; f += direction) {if (arr[f][c] != 0) {return f;}
}
} else { // 向下按键 f--
for (var f = start; f >= condition; f += direction) {if (arr[f][c] != 0) {return f;}
}
}
} else { // 左右按键
if (direction == 1) { // 左按键 f++
for (var f = start; f < condition; f += direction) {if (arr[r][f] != 0) {return f;}
}
} else { // 右按键 f--
for (var f = start; f >= condition; f += direction) {if (arr[r][f] != 0) {return f;}
}
}
}
return null; // 循环结束仍然没有找到!= 0 的数值,返回 null
},
// 左按键的处理
dealToLeft: function(r) {
var next;
for (c = 0; c < obj.ROW; c++) {next = obj.find(r, c, c + 1, obj.CELL, 1); // 找出第一个不为 0 的位置
if (next == null) break; // 没有找到就返回
// 如果当前位置为 0
if (arr[r][c] == 0) {arr[r][c] = arr[r][next]; // 把找到的不为 0 的数值替换为当前位置的值
arr[r][next] = 0; // 找到的位置清 0
c--; // 再次循环多一次,查看后面否有值与替换后的值相同,} else if (arr[r][c] == arr[r][next]) { // 如果当前位置与找到的位置数值相等,则相加
arr[r][c] *= 2;
arr[r][next] = 0;
obj.score += arr[r][c];
}
}
},
move: function(itertor) {
var before, // 没处理前
after; //after 处理后
before = arr.toString();
itertor(); // 执行 for 函数
after = arr.toString();
if (before != after) { // 前后对比,如果不同就 update
obj.random();
obj.updateView();}
},
moveLeft: function() {obj.move(function() {for (r = 0; r < obj.ROW; r++) {obj.dealToLeft(r);
}
})
// if 当前位置 不为零
// 从当前位置,下一个成员开始,遍历,// 如果找到,与当前位置相等的数,// 两者相加,并把不为零的成员,设置为零
// 如果 当前位置是 零
// 从当前位置下一个成员开始遍历
// 如果找到 第一个不为零的成员
// 当前位置数值设置为这个不为零的成员的值,并且把那个不为零的成员设置为 0
},
// 右按键处理
dealToRight: function(r) {
var next;
for (c = obj.CELL - 1; c >= 0; c--) {next = obj.find(r, c, c - 1, 0, -1); // 找出第一个不为 0 的位置
if (next == null) break; // 没有找到就返回
// 如果当前位置为 0
if (arr[r][c] == 0) {arr[r][c] = arr[r][next]; // 把找到的不为 0 的数值替换为当前位置的值
arr[r][next] = 0; // 找到的位置清 0
c++; // 再次循环多一次,查看后面否有值与替换后的值相同,} else if (arr[r][c] == arr[r][next]) { // 如果当前位置与找到的位置数值相等,则相加
arr[r][c] *= 2;
arr[r][next] = 0;
obj.score += arr[r][c];
}
}
},
moveRight: function() {obj.move(function() {for (r = 0; r < obj.ROW; r++) {obj.dealToRight(r);
}
})
},
// 上按键处理
dealToUp: function(c) {
var next;
for (r = 0; r < obj.ROW; r++) {next = obj.find(r, c, r + 1, obj.ROW, 1); // 找出第一个不为 0 的位置
if (next == null) break;
// 如果当前位置为 0
if (arr[r][c] == 0) {arr[r][c] = arr[next][c]; // 把找到的不为 0 的数值替换为当前位置的值
arr[next][c] = 0; // 找到的位置清 0
r--; // 再次循环多一次,查看后面否有值与替换后的值相同
} else if (arr[r][c] == arr[next][c]) { // 如果当前位置与找到的位置数值相等,则相加
arr[r][c] *= 2;
arr[next][c] = 0;
obj.score += arr[r][c];
}
}
},
moveUp: function() {obj.move(function() {for (c = 0; c < obj.CELL; c++) {obj.dealToUp(c);
}
})
},
// 下按键处理
dealToDown: function(c) {
var next;
for (r = obj.ROW - 1; r >= 0; r--) {next = obj.find(r, c, r - 1, 0, -1); // 找出第一个不为 0 的位置
if (next == null) {break;}
// 如果当前位置为 0
if (arr[r][c] == 0) {arr[r][c] = arr[next][c]; // 把找到的不为 0 的数值替换为当前位置的值
arr[next][c] = 0; // 找到的位置清 0
r++; // 再次循环多一次,查看后面否有值与替换后的值相同
} else if (arr[r][c] == arr[next][c]) { // 如果当前位置与找到的位置数值相等,则相加
arr[r][c] *= 2;
arr[next][c] = 0;
obj.score += arr[r][c];
}
}
},
moveDown: function() {obj.move(function() {for (c = 0; c < obj.CELL; c++) {obj.dealToDown(c);
}
})
}
}
window.onload = function() {
obj.createEle = 1;
obj.gameStart();}
// 切换模式
function getModel(e) { // 事件冒泡获取 a 元素
var a = e.target,
modelValue = 4;
if (a.nodeName == "A") {if (a.innerHTML == "3X3") {modelValue = 3;} else if (a.innerHTML == "4X4") {modelValue = 4;} else if (a.innerHTML == "5X5") {modelValue = 5;} else if (a.innerHTML == "6X6") {modelValue = 6;}
obj.ROW = obj.CELL = modelValue;
obj.createEle = 1; // 需要创建格子 div 元素的标志
obj.gameStart();}
}
</script>
</body>
</html>
正文完



