棋盘的定义
let D = 4;//阶数
let N = [2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536, 131072];
一些变量
let board = $("#board");
let Square = N;
let isUndo = false;//是否撤销中
let data = [];//当前棋盘
let dataList = [];//棋盘备份
let scoreList = [];//分数备份
let undo = $("#undo");//撤销按钮
let auto = $("#auto");//自动移动按钮
let isMoving = false, action;
let time = 50;//自动移动间隔
let backupLimit = 10;//棋盘备份上限
自定义方块名
let isUpdating = false;//是否正在更换方块名
$(".square-group").change(function () {
let val = $("input[name='square']:checked").val();
switch (val) {
case "N":
//经典版
Square = N;
break;
default:
Square = N;
}
isUpdating = true;
game.dataView();
});
动态css
/**
* 获取方块内文字长度
*/
function getLen(str) {
let len = str.length;
for (let i = 0; i < len; i++) {
if ((str.charCodeAt(i) & 0xff00) !== 0) {
len++;
}
}
return len;
}
/**
* 动态css
*/
function css() {
let width = document.body.clientWidth;//容器宽度
let height = window.innerHeight;//窗口高度
let bodyWidth = Math.min(width, (height - 55) * 0.7);
board.css("width", `${bodyWidth}px`);
board.css("height", `${bodyWidth}px`);
let M = bodyWidth / 6 / D;//方块边距
let W = (bodyWidth - M * (D + 1)) / D;//方块宽度
board.css("borderRadius", `${1.25 * M}px`);
let cells = $(".cell");
for (let i = 0; i < cells.length; i++) {
let len = getLen(cells[i].innerText);
let br = false;
if (cells[i].innerHTML.includes('<br>')) {
br = true;
len /= 4;
len = Math.round(len);
len *= 2;
}
cells[i].style.width = W + 'px';
cells[i].style.height = W + 'px';
cells[i].style.borderRadius = M / 2 + 'px'
if (len < 4) {
//1~3个字符时和3字符大小相等
cells[i].style.fontSize = W * 0.55 + 'px';
} else {
cells[i].style.fontSize = W * 1.65 / len + 'px';
}
if (br) {
cells[i].style.lineHeight = W / 2 + 'px'
} else {
cells[i].style.lineHeight = W + 'px'
}
cells[i].style.margin = M + 'px 0 0 ' + M + 'px'
}
}
游戏结束的遮罩文字
/**
* 游戏结束的遮罩文字
*/
function messageCss() {
let width = board.width();
let message = $("#message");
let x = 0.12;
message.css("fontSize", `${width * x}px`);
let h = message.height();
message.css("marginTop", `${(width - h) / 2}px`);
}
/**
* 清除游戏结束的遮罩
*/
function clear() {
$("#message-box").css({"display": "none"});
$("#message-box p").textContent = "";
}
窗口大小变化时,重新测量
window.onresize = function () {
css();
if (game.isGameOver()) {
messageCss();
}
}
刷新棋盘
/**
* 刷新棋盘
*/
function refresh() {
//遍历棋盘
let parent = $("#grid")[0];
parent.innerHTML = "";//先清空
for (let r = 0; r < D; r++) {
for (let c = 0; c < D; c++) {
let div = document.createElement("div");
div.setAttribute("class", "cell");
div.setAttribute("id", "n" + r + c);
parent.appendChild(div);
}
}
}
其他方法
/**
* 获取数组元素的下标
*/
function getArrayIndex(arr, obj) {
let i = arr.length;
while (i--) {
if (arr[i] === obj) {
return i;
}
}
return -1;
}
撤销
/**
* 撤销
*/
function Undo() {
if (dataList.length > 1) {
isUndo = true;
if (game.status === 0) {
game.status = 1;//起死回生
auto.attr("disabled", false);//自动可用
clear();
}
dataList.pop();
scoreList.pop();
if (dataList.length === 1) {
undo.attr("disabled", true);//撤销不可用
}
game.score = scoreList[scoreList.length - 1];
game.dataView();
} else {
//使用快捷键时
commonUtil.message("撤销次数已用尽!", "warning");
}
}
undo.on("click", function () {
Undo();
});
自动移动
/**
* 停止移动
*/
function stopMove() {
isMoving = false;
auto.html("自动移动");
clearInterval(action);
}
/**
* 自动移动
*/
auto.on("click", function () {
if (!isMoving) {
//开始
isMoving = true;
$(this).html("停止移动")
action = setInterval(function () {
if (game.canLeft) {
game.moveLeft();
} else {
game.moveRight();
}
game.moveUp();
}, time)
} else {
stopMove();//停止
}
});
保存和导入棋盘(代码略)
游戏结束
function gameOver() {
stopMove();
auto.attr("disabled", true);//自动不可用
$("#message-box").css({"display": "block"});
$("#message").html("无路可走了!<br>试试撤销?");
messageCss();
}
function youWin() {
stopMove();
auto.attr("disabled", true);//自动不可用
$("#message-box").css({"display": "block"});
$("#message").html("你赢了!");
messageCss();
}
游戏环节(核心代码)
/**
* 游戏环节
*/
const game = {
score: 0,//开始时分数为0
gameOver: 0,//游戏结束时游戏状态为0
status: 1,//个人状态与游戏状态相对应,默认为1
isExistsBlank: true,
canLeft: true,
canRight: true,
canUp: true,
canDown: true,
//游戏开始时还原所有
start: function () {
refresh();
dataList.length = 0;
scoreList.length = 0;
undo.html("撤销(0)");
undo.attr("disabled", true);//撤销不可用
let arr = [];
for (let i = 0; i < D; i++) {
arr[i] = new Array(i);
for (let j = 0; j < D; j++) {
arr[i][j] = 0;
}
}
data = arr;
this.score = 0;
this.status = 1;
//赋值
let type = $("input[name='type']:checked").val();
if (type === "4") {
//铺满
for (let i = 0; i < D * D; i++) {
this.randomNum();
}
} else {
for (let i = 0; i < D + 1; i++) {
this.randomNum();
}
}
//更新视图
this.dataView();
stopMove();
auto.attr("disabled", false);//自动可用
clear();
},
//载入游戏
load: function (arr, score) {
refresh();
dataList.length = 0;
scoreList.length = 0;
undo.html("撤销(0)");
undo.attr("disabled", true);//撤销不可用
data = arr;
this.score = score;
this.status = 1;
//更新视图
this.dataView();
stopMove();
auto.attr("disabled", false);//自动可用
clear();
},
//随机赋值
randomNum: function () {
let type = $("input[name='type']:checked").val();
if (type === "4") {
//布满全局
for (let r = 0; r < D; r++) {
for (let c = 0; c < D; c++) {
if (data[r][c] === 0) {
data[r][c] = 2;//禁止随机
}
}
}
this.isExistsBlank = false;
} else {
//循环获取是否方块存在空白位置
while (true) {
//获取随机值
const r = Math.floor(Math.random() * D);//随机生成一个行
const c = Math.floor(Math.random() * D);//随机生成一个列
//判断
if (data[r][c] === 0) {
switch (type) {
case "1":
//经典模式,随机生成2或者4
data[r][c] = Math.random() > 0.2 ? 2 : 4;
break;
case "2":
//只生成2
data[r][c] = 2;
break;
case "3":
//只生成棋盘中最小的方块
let min = Infinity;
for (let i = 0; i < data.length; i++) {
for (let j = 0; j < data[i].length; j++) {
if (data[i][j] !== 0 && data[i][j] < min) {
min = data[i][j];
}
}
}
if (min === Infinity) {
data[r][c] = 2;
} else {
data[r][c] = min;
}
break;
default:
//经典模式,随机生成2或者4
data[r][c] = Math.random() > 0.2 ? 2 : 4;
}
break;
}
}
}
},
//更新视图的方法
dataView: function () {
let temp;
if (isUndo) {
temp = dataList[dataList.length - 1];
} else {
temp = data;
}
//直接大循环
for (let r = 0; r < D; r++) {
//循环行
for (let c = 0; c < D; c++) {
//循环里面的每个单元格
//找到对应的id的div
const div = $("#n" + r + c)[0];//字符串拼接
if (temp[r][c] !== 0) {
div.innerHTML = Square[getArrayIndex(N, temp[r][c])];//自定义文字展示到div中
div.className = "cell n" + temp[r][c];//设置对应div的样式
} else {
div.innerHTML = "";//等于0的时候,div里面的内容直接置空
div.className = "cell";
}
}
}
if (isUpdating) {
//正在更换方块名
isUpdating = false;
css();
return;
}
//更新分数
$("#score")[0].innerHTML = this.score;
//判断游戏是否结束
if (!isUndo) {
if (this.status === this.gameOver) {
gameOver();
}
let type = $("input[name='type']:checked").val()
//游戏胜利的判断
let boardMax = 0;
for (let i = 0; i < data.length; i++) {
for (let j = 0; j < data[i].length; j++) {
if (data[i][j] !== 0 && data[i][j] > boardMax) {
boardMax = data[i][j];
}
}
}
switch (type) {
case "1":
//经典模式,合成数达到2^(N^2+1)就胜利
if (boardMax >= Math.pow(2, D * D + 1)) {
this.status = this.gameOver;
youWin();
}
break;
case "2":
case "4":
//“只出2”模式,合成数达到2^(N^2)就胜利
if (boardMax >= Math.pow(2, D * D)) {
this.status = this.gameOver;
youWin();
}
break;
case "3":
//“只出最小”模式,只有合成数达到上限才会赢
if (boardMax >= N[N.length - 1]) {
this.status = this.gameOver;
youWin();
}
break;
}
if (dataList.length > backupLimit) {
dataList.shift();
}
if (scoreList.length > backupLimit) {
scoreList.shift();
}
dataList.push(JSON.parse(JSON.stringify(data)));//记录棋盘
scoreList.push(this.score);//记录分数
} else {
//撤销中
data = JSON.parse(JSON.stringify(temp));//重要!再次深度拷贝
this.test1();
this.test2();
this.test3();
isUndo = false;
}
undo.html("撤销(" + (dataList.length - 1) + ")");
if (dataList.length > 1) {
undo.attr("disabled", false);//撤销可用
}
css();
},
//判断游戏是否结束的方法
isGameOver: function () {
//注:原代码三种情况同时判断,这里区分开
this.test1();
this.test2();
this.test3();
return !this.isExistsBlank && !this.canLeft && !this.canRight && !this.canUp && !this.canDown;//上面情况全都不符合,表示游戏已经GG了,返回一个true
},
test1: function () {
//是否存在空白
for (let r = 0; r < D; r++) {
for (let c = 0; c < D; c++) {
if (data[r][c] === 0) {
this.isExistsBlank = true;
return;
}
}
}
this.isExistsBlank = false;
},
test2: function () {
//能否左右移动
if (!this.isExistsBlank) {
for (let r = 0; r < D; r++) {
for (let c = 0; c < D; c++) {
if (c < D - 1) {
//判断左右是否有相同,只需要判断到第n-1个格子即可
if (data[r][c] === data[r][c + 1]) {
this.canLeft = true;
this.canRight = true;
return;
}
}
}
}
} else {
this.canLeft = false;
this.canRight = false;
for (let r = 0; r < D; r++) {
this.test(data[r], "row");
}
return;
}
this.canLeft = false;
this.canRight = false;
},
test3: function () {
//能否上下移动
if (!this.isExistsBlank) {
for (let r = 0; r < D; r++) {
for (let c = 0; c < D; c++) {
if (r < D - 1) {
//判断上下是否有相同,只需要判断到第n-1个格子即可
if (data[r][c] === data[r + 1][c]) {
this.canUp = true;
this.canDown = true;
return;
}
}
}
}
} else {
this.canUp = false;
this.canDown = false;
const getColumns = (arr, i) => arr.map(row => row[i]);
for (let r = 0; r < D; r++) {
this.test(getColumns(data, r), "col");
}
return;
}
this.canUp = false;
this.canDown = false;
},
test: function (a, type) {
//数组的判断,适用于未填满的棋盘
let len = a.length;
let start = a.indexOf(0);
let end = a.lastIndexOf(0);
let count = 0;
//先判断是否有相邻的相同方块
if (type === "row") {
for (let i = 0; i < len; i++) {
if (a[i] === 0) {
count++;
}
if (i < len - 1) {
//判断左右是否有相同,只需要判断到第n-1个格子即可
if (a[i] !== 0 && a[i] === a[i + 1]) {
this.canLeft = true;
this.canRight = true;
return;
}
}
}
if (count === len) {
return;
}
if (start === -1 && end === -1) {
return;
} else if (start > 0 && end === len - 1 && count === end - start + 1) {
this.canRight = true;
return;
} else if (start === 0 && end < len - 1 && count === end - start + 1) {
this.canLeft = true;
return;
} else {
this.canLeft = true;
this.canRight = true;
return;
}
}
if (type === "col") {
for (let i = 0; i < len; i++) {
if (a[i] === 0) {
count++;
}
if (i < len - 1) {
//判断上下是否有相同,只需要判断到第n-1个格子即可
if (a[i] !== 0 && a[i] === a[i + 1]) {
this.canUp = true;
this.canDown = true;
return;
}
}
}
if (count === len) {
return;
}
if (start === -1 && end === -1) {
} else if (start > 0 && end === len - 1 && count === end - start + 1) {
this.canDown = true;
} else if (start === 0 && end < len - 1 && count === end - start + 1) {
this.canUp = true;
} else {
this.canUp = true;
this.canDown = true;
}
}
},
//移动的方法
//数字左移
moveLeft: function () {
//移动之前转化一次字符串
const before = String(data);
//循环每行数据
for (let r = 0; r < D; r++) {
//处理每一行的函数
this.moveLeftInRow(r);
}
//移动之后转换一次
const after = String(data);
//判断
if (before !== after) {
//生成方块
this.randomNum();
if (this.isGameOver()) {
//如果游戏结束
this.status = this.gameOver;//自己状态等于游戏结束状态
}
//更新视图
this.dataView();
}
},
//处理每一行的数据
moveLeftInRow: function (r) {
//循环获取后面的数据,最左边不用考虑
for (let c = 0; c < D - 1; c++) {
//变量接收
const nextC = this.moveLeftNum(r, c);
//判断是否为-1,否则则为找到数字
if (nextC !== -1) {
if (data[r][c] === 0) {
//如果当前的数等于0,则当前的数和找到的数进行比较
data[r][c] = data[r][nextC];
//找到的数清空变为0
data[r][nextC] = 0;
//再次从最左边的数进行循环
c--;
} else if (data[r][c] === data[r][nextC]) {
//如果当前的数等于找到的数,则相加
data[r][c] *= 2;
//找到的数清空变为0
data[r][nextC] = 0;
//合成的数增加到得分
this.score += data[r][c];
}
} else {
//如果没有找到数,则退出循环
break;
}
}
},
moveLeftNum: function (r, c) {
//左移
//循环获取后面的数据,最左边不用考虑
for (let i = c + 1; i < D; i++) {
//判断后面是否找到数字
if (data[r][i] !== 0) {
//返回下标
return i;
}
}
//如果没有找到,返回
return -1;
},
//移动的方法
//数字右移
moveRight: function () {
//移动之前转化一次字符串
const before = String(data);
//循环每行数据
for (let r = 0; r < D; r++) {
//处理每一行的函数
this.moveRightInRow(r);
}
//移动之后转换一次
const after = String(data);
//判断
if (before !== after) {
//生成方块
this.randomNum();
if (this.isGameOver()) {
//如果游戏结束
this.status = this.gameOver;//自己状态等于游戏结束状态
}
//更新视图
this.dataView();
}
},
//处理每一行的数据
moveRightInRow: function (r) {
//循环获取前面的数据,最左边不用考虑
for (let c = D - 1; c >= 0; c--) {
//变量接收
const nextC = this.moveRightNum(r, c);
//判断是否为-1,否则则为找到数字
if (nextC !== -1) {
if (data[r][c] === 0) {
//如果当前的数等于0,则当前的数和找到的数进行比较
data[r][c] = data[r][nextC];
//找到的数清空变为0
data[r][nextC] = 0;
//再次从最右边的数进行循环
c++;
} else if (data[r][c] === data[r][nextC]) {
//如果当前的数等于找到的数,则相加
data[r][c] *= 2;
//找到的数清空变为0
data[r][nextC] = 0;
//合成的数增加到得分
this.score += data[r][c];
}
} else {
//如果没有找到数,则退出循环
break;
}
}
},
moveRightNum: function (r, c) {
//右移
//循环获取前面的数据,最右边不用考虑
for (let i = c - 1; i >= 0; i--) {
//判断前面是否找到数字
if (data[r][i] !== 0) {
//返回下标
return i;
}
}
//如果没有找到,返回
return -1;
},
//移动的方法
//数字上移
moveUp: function () {
//移动之前转化一次字符串
const before = String(data);
//循环每行数据
for (let c = 0; c < D; c++) {
//处理每一行的函数
this.moveUpInRow(c);
}
//移动之后转换一次
const after = String(data);
//判断
if (before !== after) {
//生成方块
this.randomNum();
if (this.isGameOver()) {
//如果游戏结束
this.status = this.gameOver;//自己状态等于游戏结束状态
}
//更新视图
this.dataView();
}
},
//处理每一行的数据
moveUpInRow: function (c) {
//循环获取前面的数据,最上面不用考虑
for (let r = 0; r < D - 1; r++) {
//变量接收
const nextR = this.moveUpNum(r, c);
//判断是否为-1,否则则为找到数字
if (nextR !== -1) {
if (data[r][c] === 0) {
//如果当前的数等于0,则当前的数和找到的数进行比较
data[r][c] = data[nextR][c];
//找到的数清空变为0
data[nextR][c] = 0;
//再次从最上面的数进行循环
r--;
} else if (data[r][c] === data[nextR][c]) {
//如果当前的数等于找到的数,则相加
data[r][c] *= 2;
//找到的数清空变为0
data[nextR][c] = 0;
//合成的数增加到得分
this.score += data[r][c];
}
} else {
//如果没有找到数,则退出循环
break;
}
}
},
moveUpNum: function (r, c) {
//上移
//循环获取上面的数据,最右边不用考虑
for (let i = r + 1; i < D; i++) {
//判断下面是否找到数字
if (data[i][c] !== 0) {
//返回下标
return i;
}
}
//如果没有找到,返回
return -1;
},
//移动的方法
//数字下移
moveDown: function () {
//移动之前转化一次字符串
const before = String(data);
//循环每行数据
for (let c = 0; c < D; c++) {
//处理每一行的函数
this.moveDownInRow(c);
}
//移动之后转换一次
const after = String(data);
//判断
if (before !== after) {
//生成方块
this.randomNum();
if (this.isGameOver()) {
//如果游戏结束
this.status = this.gameOver;//自己状态等于游戏结束状态
}
//更新视图
this.dataView();
}
},
//处理每一行的数据
moveDownInRow: function (c) {
//循环获取前面的数据,最下面不用考虑
for (let r = D - 1; r >= 0; r--) {
//变量接收
const nextR = this.moveDownNum(r, c);
//判断是否为-1,否则则为找到数字
if (nextR !== -1) {
if (data[r][c] === 0) {
//如果当前的数等于0,则当前的数和找到的数进行比较
data[r][c] = data[nextR][c];
//找到的数清空变为0
data[nextR][c] = 0;
//再次从最下面的数进行循环
r++;
} else if (data[r][c] === data[nextR][c]) {
//如果当前的数等于找到的数,则相加
data[r][c] *= 2;
//找到的数清空变为0
data[nextR][c] = 0;
//合成的数增加到得分
this.score += data[r][c];
}
} else {
//如果没有找到数,则退出循环
break;
}
}
},
moveDownNum: function (r, c) {
//下移
//循环获取前面的数据,最下面不用考虑
for (let i = r - 1; i >= 0; i--) {
//判断上面是否找到数字
if (data[i][c] !== 0) {
//返回下标
return i;
}
}
//如果没有找到,返回
return -1;
}
};
移动端的滑动处理
/**
* 根据起点终点返回方向
* @return 1向上 2向下 3向左 4向右 0未滑动
*/
function getDirection(startX, startY, endX, endY) {
const X = endX - startX;
const Y = endY - startY;
//滑动距离太短
if (Math.abs(X) < 2 && Math.abs(Y) < 2) {
return 0;
}
if (Y < 0 && Math.abs(Y) > Math.abs(X)) {
return 1;
} else if (Y > 0 && Math.abs(Y) > Math.abs(X)) {
return 2;
} else if (X < 0 && Math.abs(X) > Math.abs(Y)) {
return 3;
} else if (X > 0 && Math.abs(X) > Math.abs(Y)) {
return 4;
}
}
let startX, startY;
/**
* 手指接触屏幕
*/
board[0].addEventListener("touchstart", function (e) {
e.preventDefault();//防止滚动页面
startX = e.touches[0].pageX;
startY = e.touches[0].pageY;
}, {passive: false});//手指离开屏幕
/**
* 手指离开屏幕
*/
board[0].addEventListener("touchend", function (e) {
e.preventDefault();//防止滚动页面
if (game.status !== game.gameOver) {
let endX, endY;
endX = e.changedTouches[0].pageX;
endY = e.changedTouches[0].pageY;
const direction = getDirection(startX, startY, endX, endY);
switch (direction) {
case 1:
game.moveUp();
break;
case 2:
game.moveDown()
break;
case 3:
game.moveLeft();
break;
case 4:
game.moveRight()
break;
default:
break;
}
}
}, {passive: false});
键盘控制
/**
* 键盘控制
*/
document.addEventListener("keydown", function (e) {
//撤销:Ctrl+Z
if (e.ctrlKey && e.key === "z") {
Undo();
}
//上下左右
if (game.status !== game.gameOver) {
switch (e.key) {
case "ArrowUp":
e.preventDefault();
game.moveUp();
break;
case "ArrowDown":
e.preventDefault();
game.moveDown()
break;
case "ArrowLeft":
e.preventDefault();
game.moveLeft();
break;
case "ArrowRight":
e.preventDefault();
game.moveRight()
break;
}
}
});
最后,开始游戏!
game.start();//游戏开始