JavaScript实现2048小游戏
首先要明白该游戏怎么玩,即
在 4*4 的16宫格中,您可以选择上、下、左、右四个方向进行操作,数字会按方向移动,相邻的两个数字相同就会合并,组成更大的数字,每次移动或合并后会自动增加一个数字。当16宫格中没有空格子,且四个方向都无法操作时,游戏结束。
首先,把16宫格看成是矩阵的形式
游戏开始时,随机生成两个数字,2或者4,出现在矩阵中任意位置
这部分是通过类名emptyItem
及nonEmptyItem
来实现的。
步骤:
① 随机生成一个数字2或者4
② 获取所有空元素(类名emptyItem
)
③ 随机选择一个空元素,将生成的数字填充到空元素中,并将类名emptyItem
移除,添加类名nonEmptyItem
,即非空元素
④ 重复①、②、③步,再随机生成一个数字填充到随机的位置。
游戏的核心在于移动
移动有四个方向:上、下、左、右。实现思路如下:
如果触发向左移动
遍历所有非空元素
如果当前元素在第一个位置
不动
如果当前元素不在第一个位置
如果当前元素左侧是空元素
向左移动
如果当前元素左侧是非空元素
如果左侧元素和当前元素的内容不同
不动
如果左侧元素和当前元素的内容相同
向左合并
如果触发向右移动
遍历所有非空元素
如果当前元素在最后一个位置
不动
如果当前元素不在最后一个位置
如果当前元素右侧是空元素
向右移动
如果当前元素右侧是非空元素
如果右侧元素和当前元素的内容不同
不动
如果右侧元素和当前元素的内容相同
向右合并
向上移动 和 向下移动的思路同上。
判断游戏是否结束
获取所有元素
获取所有非空元素
如果所有元素的个数 == 所有非空元素的个数
循环遍历所有非空元素
上面元素存在 && (当前元素的内容 == 上面元素的内容) return
下面元素存在 && (当前元素的内容 == 下面元素的内容) return
左边元素存在 && (当前元素的内容 == 左边元素的内容) return
右边元素存在 && (当前元素的内容 == 右边元素的内容) return
以上条件都不满足,Game Over!
来看效果图
可以看到,游戏框上方是一些文字描述,游戏框外面是一个大的灰色的div,里面设定有4行,每行有4个单元格,一共是16个单元格。使用bootstrap的栅栏布局可以写成div.container>div.main>h2.title+h3.highestScore+h3.currentScore+div.panel.col-md-6>(div.row>(div.item.emptyItem)*4)*4,其中panel样式的div就是表示游戏框,row样式表示每一行,row里的每一行用小div来充当单元格,辅助说明的样式使用item表示单元格,emptyItem样式表示空单元格,nonEmptyItem表示非空单元格,然后根据矩阵中单元格的位置,再给各自的单元格添加样式,如x0y0,x1y0......根据这些样式,单独给每个单元格自定义一个x属性和y属性,x=“0”,y=“0”......具体作用在JS中体现。
html代码
<div class="container">
<div class="main">
<h2 class="title">2048小游戏</h2>
<h3 class="highestScore" >最高得分:<span id="highestScore"></span></h3>
<h3 class="center">当前得分:<span id="score" class="score"></span></h3>
<div class="panel col-md-offset-3 col-md-6 ">
<div class="row">
<div class="item emptyItem x0y0" x="0" y="0"></div>
<div class="item emptyItem x0y1" x="0" y="1"></div>
<div class="item emptyItem x0y2" x="0" y="2"></div>
<div class="item emptyItem x0y3" x="0" y="3"></div>
</div>
<div class="row">
<div class="item emptyItem x1y0" x="1" y="0"></div>
<div class="item emptyItem x1y1" x="1" y="1"></div>
<div class="item emptyItem x1y2" x="1" y="2"></div>
<div class="item emptyItem x1y3" x="1" y="3"></div>
</div>
<div class="row">
<div class="item emptyItem x2y0" x="2" y="0"></div>
<div class="item emptyItem x2y1" x="2" y="1"></div>
<div class="item emptyItem x2y2" x="2" y="2"></div>
<div class="item emptyItem x2y3" x="2" y="3"></div>
</div>
<div class="row">
<div class="item emptyItem x3y0" x="3" y="0"></div>
<div class="item emptyItem x3y1" x="3" y="1"></div>
<div class="item emptyItem x3y2" x="3" y="2"></div>
<div class="item emptyItem x3y3" x="3" y="3"></div>
</div>
</div>
<div class="col-md-offset-3 col-md-6 score">
<span>请通过键盘方向键进行操作</span>
<button id="refreshButton" class="btn btn-danger col-md-offset-2">刷新</button>
</div>
</div>
</div>
在css代码中首先进行css初始化,然后根据效果图,自行配置。
在js文件中,首先进入游戏初始化函数gameInit(),
在gameInit()中先将分数初始化,然后给“刷新”按钮绑定监听事件指向gameRefresh()函数,然后调用两次newItem函数添加两个随机元素(2或4),然后调用refreColor()函数遍历单元格,根据单元格文本值刷新背景色。
在newItem()函数中,根据一个包含2和4的数组,随机选出一个,然后遍历所有空白单元格(有样式emptyItem的)然后随机选择一个,添加样式nonEmptyItem,删除样式emptyItem,然后修改文本值为2或4的随机数。
在refreshColor()函数中,根据样式item,将所有单元格依据文本值进行背景颜色的修改。
全局添加键盘响应事件,根据keyCode的值,选择方向键的不同事件,先将isNewRndItem=false;表明每次移动之前将产生新元素的标记置为false,调用move函数和isGameOver()函数。
在move()函数中,首先遍历所有非空的单元格(有nonEmptyItem样式),根据传入的方向direction参数,决定正反向遍历,其中正向遍历为左和上,反向遍历为右和下。
对于左移动和上移动,需要正向遍历有数值的单元格,当需要移动的时候,下标小的元素可以先往上移动或往左移动,
反之,对于右移动和下移动,需要反向遍历单元格,让下标大的单元格先移动,如果下标小的先移动,如果同一行有单元格的话,小的单元格会被大单元格挡住。在遍历的每个单元格中,调用moveItem()函数。
在moveItem()函数中,首先调用getSideItem()函数,得到其旁边元素的信息(有或无,有值或空),根据旁边元素的样式和文本值,进行操作(移动,合并等)。
其余函数自行查看。
window.onload=function(){
var isNewRndItem=false;//是否产生新元素
var score=0;//初始分数
var maxScore=0//最高分数
if(localStorage.maxScore){//如果保存了最高分数
maxScore=localStorage.maxScore-0;
}
else{
maxScore=0;
}
//初始化游戏
gameInit();
//初始化设置
function gameInit(){
//初始化分数
document.getElementById('score').innerHTML=score;
//初始化最高分数
document.getElementById('highestScore').innerHTML=maxScore;
//为刷新按钮绑定刷新事件
document.getElementById('refreshButton').onclick=gameRefresh;
//初始化随机添加两个元素
newItem();
newItem();
//刷新颜色
refreshColor();
}
//重新开始游戏
function gameRefresh(){
//将所有单元格的nonEmptyItem样式清除并且数值清空
var items=document.getElementsByClassName('item');
for(var i=0;i<items.length;i++){
items[i].innerHTML='';
items[i].classList.remove('nonEmptyItem');
items[i].classList.add('emptyItem');
}
//将分数重置为score
score=0
document.getElementById('score').innerHTML=score;
// document.getElementById('highestScore').innerHTML=localStorage.maxScore;
//调用两次生成随机元素的函数
newItem();
newItem();
//刷新颜色
refreshColor();
}
//根据当前元素获取旁边的元素
function getSideItem(currentItem,direction){
//根据当前元素自定义的xy属性来获取当前元素的位置
var currentItemX=currentItem.getAttribute('x')-0;
var currentItemY=currentItem.getAttribute('y')-0;
switch(direction){
case "left":
var sideItemX=currentItemX;
var sideItemY=currentItemY-1;
break;
case "up":
var sideItemX=currentItemX-1;
var sideItemY=currentItemY;
break;
case "right":
var sideItemX=currentItemX;
var sideItemY=currentItemY+1;
break;
case "down":
var sideItemX=currentItemX+1;
var sideItemY=currentItemY;
break;
}
var sideItem=document.getElementsByClassName("x"+sideItemX+"y"+sideItemY);
// console.log(currentItem);
console.log(sideItem);
return sideItem;
}
//元素移动
function moveItem(currentItem,direction){
//首先获得旁边元素
var sideItem=getSideItem(currentItem,direction);
// if(sideItem.length==0){
if(sideItem.length==0){
//不存在旁边元素,说明当前元素在边上,不进行操作
}
else if(sideItem[0].innerHTML==""){//存在旁边元素,且为空元素
sideItem[0].innerHTML=currentItem.innerHTML;
sideItem[0].classList.remove('emptyItem');
sideItem[0].classList.add('nonEmptyItem');
currentItem.innerHTML="";
currentItem.classList.remove('nonEmptyItem');
currentItem.classList.add('emptyItem');
moveItem(sideItem[0],direction);//继续遍历
isNewRndItem=true;
}
else if(sideItem[0].innerHTML!=currentItem.innerHTML){//存在旁边元素,不为空,但是与当前元素的数值不相同
//不移动
}
else{//存在旁边元素,不为空,与当前元素数值相同,两个相同元素合成一个大元素后需要产生两个新的元素
sideItem[0].innerHTML=(currentItem.innerHTML-0)*2;
currentItem.innerHTML="";
currentItem.classList.remove('nonEmptyItem');
currentItem.classList.add('emptyItem');
moveItem(sideItem[0],direction);
score+=(sideItem[0].innerHTML-0)*10;//更新当前分数
document.getElementById('score').innerHTML=score;
maxScore=maxScore<score?score:maxScore;
document.getElementById('highestScore').innerHTML=maxScore;//更新最高分数
localStorage.maxScore=maxScore;//更新存储中的最高分数
isNewRndItem=true;
return;
}
}
//响应键盘监听事件,接收方向参数direction
function move(direction){
//获取所有有数值的单元格
var nonEmptyItems=document.getElementsByClassName('nonEmptyItem');
//此时判断移动分为左、上/右、下
//对于左移动和上移动,需要正向遍历有数值的单元格,当需要移动的时候,下标小的元素可以先往上移动或往左移动
//反之,对于右移动和下移动,需要反向遍历单元格,让下标大的单元格先移动,如果下标小的先移动,如果同一行有单元格的话,
//小的单元格会被大单元格挡住
if(direction=='left'||direction=='up'){
for(var i=0;i<nonEmptyItems.length;i++){//正向遍历
var currentItem=nonEmptyItems[i];
moveItem(currentItem,direction);
}
}
else if(direction=='right'||direction=='down'){
for(var i=nonEmptyItems.length-1;i>=0;i--){//反向遍历
var currentItem=nonEmptyItems[i];
moveItem(currentItem,direction);
}
}
if(isNewRndItem){//遍历完一遍后产生新的元素并刷新颜色
newItem();
refreshColor();
}
}
//判断游戏是否结束,即遍历所有元素,
function isGameOver(){
var nonEmptyItems=document.getElementsByClassName('nonEmptyItem');
var allItems=document.getElementsByClassName('item');
if(nonEmptyItems.length==allItems.length){//所有单元格铺满,检查是否有可以合并的两个单元格
for(var i=0;i<nonEmptyItems.length;i++){
var currentItem=nonEmptyItems[i];
//这个单元格最上方有元素且可以合并
if(getSideItem(currentItem,"up").length!=0&&getSideItem(currentItem,"up")[0].innerHTML==currentItem.innerHTML){
return;
}
//这个单元格最左方有元素且可以合并
else if(getSideItem(currentItem,"left").length!=0&&getSideItem(currentItem,"left")[0].innerHTML==currentItem.innerHTML){
return;
}
//这个单元格最下方有元素且可以合并
else if(getSideItem(currentItem,"down").length!=0&&getSideItem(currentItem,"down")[0].innerHTML==currentItem.innerHTML){
return;
}
//这个单元格最右方有元素且可以合并
else if(getSideItem(currentItem,"right").length!=0&&getSideItem(currentItem,"right")[0].innerHTML==currentItem.innerHTML){
return;
}
}
}else{
return;
}
var x=confirm("游戏结束,是否重新开始");
if(x){
gameRefresh();
}
}
//生成新元素
function newItem(){
//创建包含2,4的数组
var newRndArr=[2,2,4];
//从数组中随机拿出元素。
var newRndNum=newRndArr[Math.floor(Math.random()*3)];//产生0、1、2表示数组下标
//便于调试,调试随机生成的数字
// console.log("新产生的元素是"+newRndNum);
//将数字随机放到空白元素位置
var emptyItems=document.getElementsByClassName('emptyItem');//获取所有空白元素单元格
console.log(emptyItems);
var newRndItem=Math.floor(Math.random()*(emptyItems.length));//选择随机一个空白单元格
// console.log("位置是"+newRndItem);
emptyItems[newRndItem].innerHTML=newRndNum;//将产生的随机数放到选好的随机的空白单元格中
emptyItems[newRndItem].classList.add('nonEmptyItem');//表示该单元格已有元素
emptyItems[newRndItem].classList.remove('emptyItem');//移除表示空元素的样式
// refreshColor();
}
//刷新颜色
function refreshColor(){
//遍历所有单元格,分为空白单元格和有数值的单元格,分别设置颜色
var items=document.getElementsByClassName('item');
for(var i=0;i<items.length;i++){
var itemsHTML=items[i].innerHTML;
switch(itemsHTML){
case '':
items[i].style.background='#ECFFFF';
break;
case '2':
items[i].style.background='#B0C4DE ';
break;
case '4':
items[i].style.background='#00FFFF';
break;
case '8':
items[i].style.background='#00FF7F';
break;
case '16':
items[i].style.background='#FFFF00';
break;
case '32':
items[i].style.background='#DAA520';
break;
case '64':
items[i].style.background='#8B4513';
break;
case '128':
items[i].style.background='#FF4500';
break;
case '256':
items[i].style.background='#696969';
break;
case '512':
items[i].style.background='#FF00FF';
break;
}
}
}
//方向键监听事件
document.onkeydown=function(e){
switch(e.keyCode){//判断按键
case 37://左方向键
console.log("左移动");
isNewRndItem=false;//不用产生新元素
move("left");//向左移动
isGameOver();//判断是否游戏结束
break;
case 38://上方向键
console.log("上移动");
isNewRndItem=false;//不用产生新元素
move("up");//向上移动
isGameOver();//判断是否游戏结束
break;
case 39://右方向键
console.log("右移动");
isNewRndItem=false;//不用产生新元素
move("right");//向右移动
isGameOver();//判断是否游戏结束
break;
case 40://下方向键
console.log("下移动");
isNewRndItem=false;//不用产生新元素
move("down");//向下移动
isGameOver();//判断是否游戏结束
break;
}
}
// 手机屏幕滑动事件
}
参考地址:https://github.com/nnngu/js_game_2048