一、前言
今年过完年来到学校步入了大三下学期,一番挣扎之后狠心放弃了学了一年多的Java,决定开始转战喜欢的前端。然后每天泡在实验室里学习前端的基础,一切从零开始,这期间刚好是各大互联网公司春招的时候就顺便往几个大厂投了的简历,申请暑期实习。2048小游戏是360奇舞学院的前端星的压轴选拔题目。刚看到这个题目时一脸懵,因为我之前没玩过2048小游戏不知道这是个什么东西【允悲】,接下来就跟大家一起分享一下我用了5天的课余时间从不知道这是个什么游戏到最后把它写出来的过程吧。[这是一道30分的题目 我得了23分 大佬评语是动画做的差了点]
二、题目描述
用JavaScript实现一个2048小游戏
题目要求:
- 实现web的2048游戏,使用JavaScript或TypeScript完成
- 基本规则、计算分数和判断终局准确无误
- 移动数字时有基本的动画效果
加分项
- 适配移动端
- 其他额外的效果或可玩性改进
- 代码实现优雅,注释完整
三、游戏设计思路
先一起来看一下项目文件结构(此项目我没用任何框架和jQuery库)
页面布局样式,先把页面基本元素展示出来(HTML+CSS完整代码)。
- HTML
2048小游戏
2048小游戏
重新开始
得分: 分
这里我做了手机端适配,这段代码可以让页面在手机端等比放大。
- CSS
body {
background-color: #ced9c5;
text-align: center;
font-size: 20px;
font-weight: bold;
font-family: "Microsoft YaHei";
}
/*header部分样式*/
header{
display: block;
margin: 0px auto;
text-align: center;
}
header h1{
font-size: 1.5em;
}
header #newgame_btn{
width: 100px;
padding:10px;
background: #7da962;
font-family:inherit;
font-size: 20px;
color: #fff;
border-radius: 10px;
text-decoration: none;
}
/*table样式*/
table {
margin: 0px auto;
margin-top: 30px;
border-radius: 10px;
background-color: #bfc0bb;
border-spacing: 8px;
}
td {
width: 80px;
height: 80px;
border-radius: 10px;
-webkit-transition: all 0.2s ease-out;
-moz-transition: all 0.2s ease-out;
-o-transition: all 0.2s ease-out;
transition: all 0.2s ease-out;
}
/*footer部分样式*/
main .number_title {
font-size: 0.8em;
}
main #show_number{
color: #f8563d;
font-size: 1.3em;
}
.inner {
height: 50px;
line-height: 1.8;
text-align: center;
padding: 15px;
font-size: .7em;
color: #656565;
}
footer .operation_pc{
border-top: 1px solid #bfc0bb;
}
footer .operation_phone{
border-bottom: 1px solid #bfc0bb;
}
棋盘棋子初始化与产生新的棋子、计分和对产生的新棋子涂色
- 通过了解游戏规则,游戏中有16个格子,游戏刚开始时需要随机在两个格子里出现两个数,接下来每次移动后要产生一个随机数,随机数只能是随机的2或4。所以这里我们就要做一个生成随机数的函数。
- 通过调用这个函数并传入最大和最小数值的参数,然后函数会返回一个介于这两个数之间(包括这两个数)的一个随机整数。
function GetRandom(Min,Max){
return Min + Math.round((Max-Min) * Math.random());
}
- 调用产生随机数的函数,在1~16的空格子里产生随机的2或4。
- 要注意的是,在产生随机的2或4时要先判断要放入随机数的格子是否是空的,如果随机找的格子不为空则不产生随机的2或4,要再次调用自身直到找到空的格子为止。
/*给随机的格子放入随机的2或4*/
function RandomNum(){
// 产生一个1~16的随机数来获取一个格子的ID
let num = GetRandom(1,16);
// 空格子数量计数器
let count = 0;
// 判断产生随机格子内是否有数字
if(document.getElementById(num).innerHTML == ""){
// 产生2或4随机数并放入随机产生的空格子中
document.getElementById(num).innerHTML = GetRandom(1,2) * 2;
}
// 如果格子有数字遍历棋盘是否放慢了棋子
else{
// 遍历棋盘是否有空格子,如果有计数器就累加1
for(let i = 1; i <= 16; i++){
let piece_ = document.getElementById(i);
if( piece_.innerHTML== ""){
count++;
}
}
// 判断棋盘是否有空格,如果有就再次执行随机产生棋子的函数
if(count > 0){
RandomNum();
}
}
}
- 接下来创建一个每次棋子移动后产生结果的函数,这里创建一个控制不同数字背景颜色的对象
let color_object = {
"":"#d3d3d3",
"2":"#fef4f2",
"4":"#fed9a2",
"8":"#fc8c5e",
"16":"#f8692f",
"32":"#f8563d",
"64":"#ff3936",
"128":"#00c3dd",
"256":"#00a4be",
"512":"#00abcb",
"1024":"#00abcb",
"2048":"#00abcb",
"4096":"#005d6e"
};
- 产生随机数和结果的函数创建后,进行函数的初始化。
function init(){
// 获取棋盘table对象
let checkerBoard = document.getElementById("checkerBoard");
let text = "";
let id = 1;
// 循环产生tr*4+td*4的行和列并给每一个tablecell赋予ID值(用1~16分别代表每个格子的ID值)
for(let i = 1; i < 5; i++){
// 拼接
text += " ";
for(let j = i; j <= i+12; j += 4){
// 拼接
text += " ";
// 使td的ID值依次递增
id++;
}
// 拼接
text += ""
}
// 把循环产生的行和列放入table中
checkerBoard.innerHTML = text;
// 在棋盘中循环放入两个由RandomNum()产生的随机位置和随机的2或4
for(let k = 0; k < 2; k++){
RandomNum();
}
Result();
}
- main.js(完整的main.js代码)
/*新打开窗口时初始化游戏*/
window.onload = init();
/*点击重新开始游戏按钮时初始化游戏*/
function newGame(){
init();
}
/*生成一个介于两个整数之间的随机整数*/
function GetRandom(Min,Max){
return Min + Math.round((Max-Min) * Math.random());
}
/*给随机的格子放入随机的2或4*/
function RandomNum(){
// 产生一个1~16的随机数来获取一个格子的ID
let num = GetRandom(1,16);
// 空格子数量计数器
let count = 0;
// 判断产生随机格子内是否有数字
if(document.getElementById(num).innerHTML == ""){
// 产生2或4随机数并放入随机产生的空格子中
document.getElementById(num).innerHTML = GetRandom(1,2) * 2;
}
// 如果格子有数字遍历棋盘是否放慢了棋子
else{
// 遍历棋盘是否有空格子,如果有计数器就累加1
for(let i = 1; i <= 16; i++){
let piece_ = document.getElementById(i);
if( piece_.innerHTML== ""){
count++;
}
}
// 判断棋盘是否有空格,如果有就再次执行随机产生棋子的函数
if(count > 0){
RandomNum();
}
}
}
/*初始化游戏数据*/
function init(){
// 获取棋盘table对象
let checkerBoard = document.getElementById("checkerBoard");
let text = "";
let id = 1;
// 循环产生tr*4+td*4的行和列并给每一个tablecell赋予ID值(用1~16分别代表每个格子的ID值)
for(let i = 1; i < 5; i++){
// 拼接
text += " ";
for(let j = i; j <= i+12; j += 4){
// 拼接
text += " ";
// 使td的ID值依次递增
id++;
}
// 拼接
text += ""
}
// 把循环产生的行和列放入table中
checkerBoard.innerHTML = text;
// 在棋盘中循环放入两个由RandomNum()产生的随机位置和随机的2或4
for(let k = 0; k < 2; k++){
RandomNum();
}
Result();
}
/*给不同数值的棋子涂上不同的颜色,并计算得分*/
function Result(){
// 给分数初始值为0
let score = 0;
// 计数棋子的数量
let count = 0;
// 定义一个数值对应颜色的对象
let color_object = {
"":"#d3d3d3",
"2":"#fef4f2",
"4":"#fed9a2",
"8":"#fc8c5e",
"16":"#f8692f",
"32":"#f8563d",
"64":"#ff3936",
"128":"#00c3dd",
"256":"#00a4be",
"512":"#00abcb",
"1024":"#00abcb",
"2048":"#00abcb",
"4096":"#005d6e"
};
for(let i = 1; i <= 16; i++){
// 遍历所有棋子,给对应数值的棋子涂上对应的颜色
let text = document.getElementById(i);
text.style.backgroundColor = color_object[text.innerHTML];
// 以棋子数值为8的棋子作为临界给数值大于等于8和小于8的棋子数字分别设置不同的颜色
if(text.innerHTML >= 8){
text.style.color = "#fff";
}else{
text.style.color = "#6e6f71";
}
// 如果格子的数值不为空就把此格子上棋子的数值累加作为当前得分
if(text.innerHTML != ""){
score += parseInt(text.innerHTML);
count++;
}
}
// 如果棋子数量为2时,即游戏刚初始化,此时分数置零
if(count == 2){
document.getElementById("show_number").innerHTML = 0;
}
// 如果棋子数量不是二则正常累加得分
else{
document.getElementById("show_number").innerHTML = score;
}
}
接下来开始让棋子动起来(pc端用方向键操作,移动端用手指滑动操作)
- 创建一个棋子移动的函数,并设两个参数。
function Change(piece_1,piece_2){
// 判断如果棋子要移动的方向有空格就将其移动
if(piece_1.innerHTML == "" && piece_2.innerHTML != ""){
res = true;
piece_1.innerHTML = piece_2.innerHTML;
piece_2.innerHTML = "";
}
// 判断如果相邻棋子都不为空并且数值相等就将他们合并,同时将一个格子置空
else if(piece_1.innerHTML != "" && piece_1.innerHTML == piece_2.innerHTML){
res = true;
piece_1.innerHTML = parseInt(piece_1.innerHTML) + parseInt(piece_2.innerHTML);
piece_2.innerHTML = "";
}
}
- 棋子往各个方向移动的函数Top()、Down()、Right()、Left()这里不再一一陈述见下面完整代码。
- 建立键盘监听来判断按压对应的方向键执行对应的移动函数,并在每次按压后调用gameOver();判断游戏是否结束。
document.onkeydown = function pc_move(){
res = false;
if(event.keyCode == 38) Top();
else if(event.keyCode == 40) Down();
else if(event.keyCode == 37) Left();
else if(event.keyCode == 39) Right();
// 判断游戏是否结束
gameOver();
// 如果棋子移动了得到的res==true,就再次产生新的棋子
if(res) RandomNum();
// 调用Result()函数,给棋子涂色并记录当前分数
Result();
}
- 建立滑动屏幕监听事件,通过判断滑动方向来执行对应的移动操作,并在每次滑动屏幕后调用gameOver();判断游戏是否结束。
- 值得注意的是在手机上滑动时屏幕默认的动作是页面滑动,所以用下面的一行代码来阻止默认事件的执行。
e.preventDefault();
function touch_move(){
// 定义滑动的起点和终点X、Y坐标值
let startX = 0;
let startY = 0;
let endX = 0;
let endY = 0;
// 获取棋盘对象
let table = document.getElementById("checkerBoard");
// 给棋盘绑定'touchstart'事件
table.addEventListener('touchstart',function(e){
let touch = event.targetTouches[0];
// 在手指点击屏幕时阻止屏幕拖动事件
e.preventDefault();
//获取手指滑动起点坐标
startX = touch.pageX;
startY = touch.pageY;
});
// 给棋盘绑定'touchmove'事件
table.addEventListener('touchmove',function(e){
let touch = event.targetTouches[0];
// 在手指滑动屏幕时阻止屏幕拖动事件
e.preventDefault();
//获取手指滑动屏幕的终点坐标
endX = touch.pageX;
endY = touch.pageY;
});
// 给棋盘绑定'touchend'事件
table.addEventListener('touchend',function(e){
// 在手指滑动屏幕结束时阻止屏幕拖动事件
e.preventDefault();
//滑动结束,计算出手指分别在X、Y轴上滑动距离
let distanceX = endX - startX;
let distanceY = endY - startY;
res = false;
// console.log("startX " + startX + " startY" + startY);
// console.log("endX" + endX + " endY" + endY);
// console.log("distanceX" + distanceX + " distanceY" + distanceY)
// console.log("------------------------------------------------------------------------------");
// 如果终点坐标X、Y值都不为零则进入下一步判断滑动方向
if(endX!=0 && endY!=0){
//向上滑动
if(Math.abs(distanceY) > Math.abs(distanceX) && distanceY < -10){
Top();
}
//向下滑动
else if(Math.abs(distanceY) > Math.abs(distanceX) && distanceY > 10){
Down();
}
//向左滑动
else if(Math.abs(distanceX) > Math.abs(distanceY) && distanceX < -10){
Left();
}
//向右滑动
else if(Math.abs(distanceX) > Math.abs(distanceY) && distanceX > 10){
Right();
}
// 判断游戏是否结束
gameOver();
// 如果棋子移动了得到的res==true,就再次产生新的棋子
if(res){
RandomNum();
}
// 将以上绑定事件获取的起始点坐标值置零
startX = startY = endX = endY = 0;
}
// 调用Result()函数,给棋子涂色并记录当前分数
Result();
});
}
touch_move();
- move.js(move.js完整代码)
// 控制棋盘是否产生随机的棋子,当res==true时产生新的棋子
let res = false;
/*移动棋子使相邻并且相同数值的棋子合并,同时将数值相加*/
function Change(piece_1,piece_2){
// 判断如果棋子要移动的方向有空格就将其移动
if(piece_1.innerHTML == "" && piece_2.innerHTML != ""){
res = true;
piece_1.innerHTML = piece_2.innerHTML;
piece_2.innerHTML = "";
}
// 判断如果相邻棋子都不为空并且数值相等就将他们合并,同时将一个格子置空
else if(piece_1.innerHTML != "" && piece_1.innerHTML == piece_2.innerHTML){
res = true;
piece_1.innerHTML = parseInt(piece_1.innerHTML) + parseInt(piece_2.innerHTML);
piece_2.innerHTML = "";
}
}
/*向上移动棋子*/
function Top(){
for(let i = 1; i < 5; i++){
for(let j = i; j <= i + 12; j += 4){
for(let k = j; k > 4; k -= 4){
let piece_1 = document.getElementById(k - 4);
let piece_2 = document.getElementById(k);
Change(piece_1,piece_2);
}
}
}
}
/*向下移动棋子*/
function Down(){
for(let i = 1; i < 5; i++){
for(let j = i + 12; j >= i; j -= 4){
for(let k = j; k < 13; k += 4){
let piece_1 = document.getElementById(k + 4);
let piece_2 = document.getElementById(k);
Change(piece_1,piece_2);
}
}
}
}
/*向左移动棋子*/
function Left(){
for(let i = 1; i <= 13; i += 4){
for(let j = i; j <= i + 3; j += 1){
for(let k = j; k > i; k -= 1){
let piece_1 = document.getElementById(k - 1);
let piece_2 = document.getElementById(k);
Change(piece_1,piece_2);
}
}
}
}
/*向右移动棋子*/
function Right(){
for(let i = 1; i <= 13; i += 4){
for(let j = i + 4; j >= i; j -= 1){
for(let k = j; k < i + 3; k += 1){
let piece_1 = document.getElementById(k + 1);
let piece_2 = document.getElementById(k);
Change(piece_1,piece_2);
}
}
}
}
/*按压键盘方向键使棋子移动*/
document.onkeydown = function pc_move(){
res = false;
if(event.keyCode == 38) Top();
else if(event.keyCode == 40) Down();
else if(event.keyCode == 37) Left();
else if(event.keyCode == 39) Right();
// 判断游戏是否结束
gameOver();
// 如果棋子移动了得到的res==true,就再次产生新的棋子
if(res) RandomNum();
// 调用Result()函数,给棋子涂色并记录当前分数
Result();
}
/*滑动手机屏幕使棋子移动*/
function touch_move(){
// 定义滑动的起点和终点X、Y坐标值
let startX = 0;
let startY = 0;
let endX = 0;
let endY = 0;
// 获取棋盘对象
let table = document.getElementById("checkerBoard");
// 给棋盘绑定'touchstart'事件
table.addEventListener('touchstart',function(e){
let touch = event.targetTouches[0];
// 在手指点击屏幕时阻止屏幕拖动事件
e.preventDefault();
//获取手指滑动起点坐标
startX = touch.pageX;
startY = touch.pageY;
});
// 给棋盘绑定'touchmove'事件
table.addEventListener('touchmove',function(e){
let touch = event.targetTouches[0];
// 在手指滑动屏幕时阻止屏幕拖动事件
e.preventDefault();
//获取手指滑动屏幕的终点坐标
endX = touch.pageX;
endY = touch.pageY;
});
// 给棋盘绑定'touchend'事件
table.addEventListener('touchend',function(e){
// 在手指滑动屏幕结束时阻止屏幕拖动事件
e.preventDefault();
//滑动结束,计算出手指分别在X、Y轴上滑动距离
let distanceX = endX - startX;
let distanceY = endY - startY;
res = false;
// console.log("startX " + startX + " startY" + startY);
// console.log("endX" + endX + " endY" + endY);
// console.log("distanceX" + distanceX + " distanceY" + distanceY)
// console.log("------------------------------------------------------------------------------");
// 如果终点坐标X、Y值都不为零则进入下一步判断滑动方向
if(endX!=0 && endY!=0){
//向上滑动
if(Math.abs(distanceY) > Math.abs(distanceX) && distanceY < -10){
Top();
}
//向下滑动
else if(Math.abs(distanceY) > Math.abs(distanceX) && distanceY > 10){
Down();
}
//向左滑动
else if(Math.abs(distanceX) > Math.abs(distanceY) && distanceX < -10){
Left();
}
//向右滑动
else if(Math.abs(distanceX) > Math.abs(distanceY) && distanceX > 10){
Right();
}
// 判断游戏是否结束
gameOver();
// 如果棋子移动了得到的res==true,就再次产生新的棋子
if(res){
RandomNum();
}
// 将以上绑定事件获取的起始点坐标值置零
startX = startY = endX = endY = 0;
}
// 调用Result()函数,给棋子涂色并记录当前分数
Result();
});
}
touch_move();
进行游戏结束判断(遍历棋盘当棋盘放满棋子,并且每个棋子上下左右相邻位置都不相同则游戏结束)
- gameOver.js(gameOver.js完整代码)
/*游戏结束判断*/
function gameOver(){
// 设置一个空格子计数器
let count = 0;
// 遍历棋盘是否有空格子,如果有计数器就累加1
for(let i = 1; i <= 16; i++){
let piece_ = document.getElementById(i);
if( piece_.innerHTML== ""){
count++;
}
}
// 判断棋盘是否摆满棋子
if(count == 0){
// 当棋盘摆满棋子时,遍历所有棋子看其与相邻的棋子数值是否相等,一旦有相等的计数器就累加1
for(let i = 1; i <= 16; i++){
let piece_self = document.getElementById(i);
let piece_add_1 = document.getElementById(i + 1);
let piece_cut_1 = document.getElementById(i - 1);
let piece_add_4 = document.getElementById(i + 4);
let piece_cut_4 = document.getElementById(i - 4);
switch (i){
case 1:
if(parseInt(piece_self.innerHTML) == parseInt(piece_add_1.innerHTML) || parseInt(piece_self.innerHTML) == parseInt(piece_add_4.innerHTML)){
count++;
}
break;
case 2:
if(parseInt(piece_self.innerHTML) == parseInt(piece_add_1.innerHTML) || parseInt(piece_self.innerHTML) == parseInt(piece_add_4.innerHTML) || parseInt(piece_self.innerHTML) == parseInt(piece_cut_1.innerHTML)){
count++;
}
break;
case 3:
if(parseInt(piece_self.innerHTML) == parseInt(piece_add_1.innerHTML) || parseInt(piece_self.innerHTML) == parseInt(piece_add_4.innerHTML) || parseInt(piece_self.innerHTML) == parseInt(piece_cut_1.innerHTML)){
count++;
}
break;
case 4:
if(parseInt(piece_self.innerHTML) == parseInt(piece_cut_1.innerHTML) || parseInt(piece_self.innerHTML) == parseInt(piece_add_4.innerHTML)){
count++;
}
break;
case 5:
if(parseInt(piece_self.innerHTML) == parseInt(piece_add_1.innerHTML) || parseInt(piece_self.innerHTML) == parseInt(piece_add_4.innerHTML) || parseInt(piece_self.innerHTML) == parseInt(piece_cut_4.innerHTML)){
count++;
}
break;
case 6:
if(parseInt(piece_self.innerHTML) == parseInt(piece_add_1.innerHTML) || parseInt(piece_self.innerHTML) == parseInt(piece_add_4.innerHTML) || parseInt(piece_self.innerHTML) == parseInt(piece_cut_1.innerHTML) || parseInt(piece_self.innerHTML) == parseInt(piece_cut_4.innerHTML)){
count++;
}
break;
case 7:
if(parseInt(piece_self.innerHTML) == parseInt(piece_add_1.innerHTML) || parseInt(piece_self.innerHTML) == parseInt(piece_add_4.innerHTML) || parseInt(piece_self.innerHTML) == parseInt(piece_cut_1.innerHTML) || parseInt(piece_self.innerHTML) == parseInt(piece_cut_4.innerHTML)){
count++;
}
break;
case 8:
if(parseInt(piece_self.innerHTML) == parseInt(piece_cut_1.innerHTML) || parseInt(piece_self.innerHTML) == parseInt(piece_add_4.innerHTML) || parseInt(piece_self.innerHTML) == parseInt(piece_cut_4.innerHTML)){
count++;
}
break;
case 9:
if(parseInt(piece_self.innerHTML) == parseInt(piece_add_1.innerHTML) || parseInt(piece_self.innerHTML) == parseInt(piece_add_4.innerHTML) || parseInt(piece_self.innerHTML) == parseInt(piece_cut_4.innerHTML)){
count++;
}
break;
case 10:
if(parseInt(piece_self.innerHTML) == parseInt(piece_add_1.innerHTML) || parseInt(piece_self.innerHTML) == parseInt(piece_add_4.innerHTML) || parseInt(piece_self.innerHTML) == parseInt(piece_cut_1.innerHTML) || parseInt(piece_self.innerHTML) == parseInt(piece_cut_4.innerHTML)){
count++;
}
break;
case 11:
if(parseInt(piece_self.innerHTML) == parseInt(piece_add_1.innerHTML) || parseInt(piece_self.innerHTML) == parseInt(piece_add_4.innerHTML) || parseInt(piece_self.innerHTML) == parseInt(piece_cut_1.innerHTML) || parseInt(piece_self.innerHTML) == parseInt(piece_cut_4.innerHTML)){
count++;
}
break;
case 12:
if(parseInt(piece_self.innerHTML) == parseInt(piece_cut_1.innerHTML) || parseInt(piece_self.innerHTML) == parseInt(piece_add_4.innerHTML) || parseInt(piece_self.innerHTML) == parseInt(piece_cut_4.innerHTML)){
count++;
}
break;
case 13:
if(parseInt(piece_self.innerHTML) == parseInt(piece_add_1.innerHTML) || parseInt(piece_self.innerHTML) == parseInt(piece_cut_4.innerHTML)){
count++;
}
break;
case 14:
if(parseInt(piece_self.innerHTML) == parseInt(piece_add_1.innerHTML) || parseInt(piece_self.innerHTML) == parseInt(piece_cut_1.innerHTML) || parseInt(piece_self.innerHTML) == parseInt(piece_cut_4.innerHTML)){
count++;
}
break;
case 15:
if(parseInt(piece_self.innerHTML) == parseInt(piece_add_1.innerHTML) || parseInt(piece_self.innerHTML) == parseInt(piece_cut_1.innerHTML) || parseInt(piece_self.innerHTML) == parseInt(piece_cut_4.innerHTML)){
count++;
}
break;
case 16:
if(parseInt(piece_self.innerHTML) == parseInt(piece_cut_1.innerHTML) || parseInt(piece_self.innerHTML) == parseInt(piece_cut_4.innerHTML)){
count++;
}
break;
default:
break;
}
}
// 当遍历所有棋子后与相邻位置棋子数值都不相等,游戏结束
if(count == 0){
// 获取当前得分
let score = document.getElementById("show_number").innerHTML;
alert("潇洒人生,极限挑战。游戏结束! 您的得分:"+score+" 分");
// 点击确定按钮后初始化游戏
init();
}
}
}
至此整个项目设计过程就结束了,由于刚转入前端代码书写肯定不是很优雅,欢迎大家对我的代码不足之处进行指正。
(寝室已经断电,室友已是鼾声四起,准备睡觉啦,明天一早还要起来去上课【允悲】)
完整项目访问 我的github仓库
点击此处 体验我的2048小游戏