- A+
前言
注:该篇记录暂未实现过渡动画以及移动端的上下左右操作
我的2048后续最新效果展示2048GAME (226yzy.com)
还有后续代码我放在了我的Github上了226YZY/my2048game: 我的简易2048小游戏 (github.com)
(本篇效果烦请自行根据代码复现?)
本篇记录中的代码是我根据我对2048的基本游戏逻辑,尝试通过html+css+原生js实现
由于个人水平有限,相关内容有不完善的地方欢迎大佬指出Orz
原版2048相关内容
原版2048首先在GitHub上发布,原作者是Gabriele Cirulli
在Github上可以找到这个这个小游戏的源代码,传送门?gabrielecirulli/2048: A small clone of 1024 (https://play.google.com/store/apps/details?id=com.veewo.a1024) (github.com)
你也可以试玩原作者制作的2048,传送门?2048 (play2048.co)
游戏基本逻辑梳理
1.页面基本内容
页面上至少需要有以下基本内容
- 4*4的表作为游戏主体
- 显示当前多少分
- 新游戏
- 提示游戏结束(一开始不显示)
- 初始随机两个格子内有数字(该数字为2或4)
- 根据格子内的数字,格子内的颜色改变
2.随机产生数字
在没有数字的格子产生一个数字
该数字只能为2或4
其中4的概率较小为10%(后来我粗略观察原版2048的代码发现的)
3.计分
每对相同的数字合并后,总分加上这对数字的和
例如2和2合并后总分加4分,8和8合并后总分加16分
4.合并数字
基础的(0表示空,下同)比如 0 2 0 2左移后应为4 0 0 0
需要注意的情况
与原版2048对比后我发现网上一些自制的2048在这方面有些问题,下面列出我注意到的情况
- 例1(是否重复合并)
2 2 2 2这行如果向左移动后结果应为4 4 0 0
也就是说,合并后的数字不与接下来相同的数字再合并
- 例2(合并顺序)
2 2 2 4右移后应为0 2 4 4
说明向右移,合并应从右侧开始
若为左移,则合并应从左侧开始,上下移动的同理
5.上下左右操作响应
对键盘事件响应,通过wsad或方向键对应上下左右操作
6.游戏是否结束或达到胜利值
在每次操作后,随即遍历每个格子上下左右是否还有相同的数字
若存在某个格子上下左右中存在与之相同的数字,那么游戏尚未结束
否则,若所有格子上下左右中都不存在与之相同的数字,则说明游戏结束
胜利值在上面遍历时查找即可
代码实现
HTML部分
html部分总览
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>2048GAME</title> <!-- css引入 --> <link href="css/main.css" rel="stylesheet"> </head> <body onkeydown="keyboardEvents()"> <!-- 当前总分显示与新游戏按钮--> <div id="theMark"> <div>当前分数为 <span id="mark"></span> 分</div> <button id="newgame" onclick="main()"><span>NEW GAME</span></button> </div> <!-- 2048游戏主体--> <!-- cellspacing属性可设置或返回在表格中的单元格之间的空白量 --> <table id="maintable" cellspacing="10"></table> <!-- 游戏结束时显示 GAME OVER--> <div id="gameover"></div> </body> <!-- js引入 --> <script src="js/main2048.js"></script> </html>
当前总分显示与新游戏按钮
该部分代码如下
<div id="theMark"> <div>当前分数为 <span id="mark"></span> 分</div> <button id="newgame" onclick="main()"><span>NEW GAME</span></button> </div>
<span id="mark"></span>
这部分的内容会后续通过js渲染上去,表示总分
<button id="newgame" onclick="main()"><span>NEW GAME</span></button>
点击后会执行js中写好的main()函数,以重新加载2048游戏的4*4表格主体以及总分归零
2048游戏主体
<!-- cellspacing属性可设置或返回在表格中的单元格之间的空白量 --> <table id="maintable" cellspacing="10"></table>
这部分我用table
实现,里面的td
通过js渲染上去,渲染后如下
<table id="maintable" cellspacing="10"> <tbody> <tr> <td id="1" ></td> <td id="2" ></td> <td id="3" >4</td> <td id="4" ></td> </tr> <tr> <td id="5" >2</td> <td id="6" ></td> <td id="7" ></td> <td id="8" ></td> </tr> <tr> <td id="9" ></td> <td id="10" ></td> <td id="11" ></td> <td id="12" ></td> </tr> <tr> <td id="13" ></td> <td id="14" ></td> <td id="15" ></td> <td id="16" ></td> </tr> </tbody> </table>
注:
原作者是用div
实现的,你也可以用这种做法
游戏结束或达到胜利值时提示
<!-- 游戏结束或达到胜利值时显示对应的内容--> <div id="gameover"></div>
该部分通过css使其初始不显示,当判断游戏结束或达到胜利值时由js更改其style和内容,使其显示
CSS部分
css部分总览
body { text-align: center;/*文本居中*/ background-color: #E0FFFF; /*背景颜色*/ } /*表格样式*/ table { min-width: 340px; font-size: 30px; font-weight: bold; background-color: #bbada0; margin: 0px auto; /*上下外边距为0,左右根据宽度自适应,即水平居中*/ margin-top: 20px; border-radius: 10px; /* 设置表格边框圆角 */ } /*单元格样式*/ td { width: 80px; height: 80px; border-radius: 10px; background-color: rgb(29, 180, 250); } /*总分提示部分字体大小和粗细*/ #theMark{ font-size: 36px; font-weight: bold; } /*突出分数,设置其字体颜色*/ #mark{ color: rgb(33, 132, 245); } /*新游戏按钮样式*/ #newgame{ background-color: rgb(241, 176, 56); border-radius: 5px; width: 150px; height: 50px; color: aliceblue; font-size: large; font-weight: 800; margin-top: 20px; } /*游戏结束提示*/ #gameover{ font-size: 36px; font-weight: bold; display:none;/*初始不显示*/ color: crimson; margin: 0 auto; }
各部分内容见上面css代码及其注释,及不过多赘述了?
JS部分
这部分重点?
路漫漫其修远兮,上面合并数字中提到的问题以及各种bug,让我重写了好几次核心部分?
1.初始
各种页面渲染及各种函数调用都基本在这一部分实现
window.onload = main(); //加载主函数 var overflag=true;//判断游戏是否结束的标志 //主函数 function main() { mapx=4,mapy=4,mapt=mapx*mapy;//设定表框大小,一般为4*4 var table = document.getElementById("maintable"); //获取表格 var tableStructure = ""; //储存表格结构 var tdid = 1; //单元格id var mark=document.getElementById("mark");//获取html中显示分数的位置 overflag=true; //因为后续新游戏是重新执行该函数,所以该值要重新变为true //加载表格结构(4行4列) for (var i = 1; i <= mapx; i++) { tableStructure += "<tr>"; //为表格加1行 //为该行添加单元格 for (var j = 1; j <= mapy; j++) { tableStructure += "<td id=" + tdid + "></td>"; //添加单元格与该单元格id tdid++; } tableStructure += "</tr>"; } table.innerHTML = tableStructure; //向表格返回该表格结构 tdRandom(); tdRandom();//随机两个格子给予2或 4 tdcolor();//渲染格子的颜色 mark.innerHTML=0;//初始总分为0 document.getElementById("gameover").style.display="none";//初始GME OVER提示不显示 document.getElementById("gameover").innerHTML=""; youwin=2048;//设置胜利值 }
原版2048为4*4的表格,我在代码中的mapx,mapy的值可以自行设置,以便魔改成更大的表格供游玩。
youwin
的值代表胜利值可自行修改
2.随机相关
这部分需要解决随机格子随机产生2或4
//因为Math.random() 返回 0(包括) 至 1(不包括) 之间的随机数,随机的数不一定是整数。这里我用向下取整 function myrandom(min, max) { return min + Math.floor(Math.random() * max); } //随机为单元格随机填入2或4 (4出现的概率应相对较小) function tdRandom() { var temp = myrandom(1, mapt); if (document.getElementById(temp).innerHTML == "") { document.getElementById(temp).innerHTML = Math.random()<0.9?2:4; } else { tdRandom(); } }
我一开始想设出现4的概率为25%,后来感觉手感不对,于是看原版的代码后发现其设置为10%
3.键盘事件响应
注:
由于所学有限,本篇的移动操作暂时只实现了键盘事件的响应,移动端的触屏操作待后续研究
//键盘事件响应 function keyboardEvents() { //以键盘方向键或wasd操作移动 if (overflag) { if (event.keyCode == 37 || event.keyCode == 65) Left(); else if (event.keyCode == 38 || event.keyCode == 87) Up(); else if (event.keyCode == 39 || event.keyCode == 68) Right(); else if (event.keyCode == 40 || event.keyCode == 83) Down(); //保证其它按键不会触发 if (flag_r) { tdRandom();//随机再产生一个数字 flag_r = false; } } tdcolor();//更新格子颜色 if(overflag) isover();//判断游戏是否结束 }
上下左右的操作函数见下文,设置flag_r
是为了限制该函数其他键响应。每按一次按键会对游戏是否结束以及格子颜色更新进行重判
4.上下左右操作函数
这部分四个操作的代码逻辑基本相同,只是根据方向的不同存入数字的顺序不同
格子编号布局顺序如下
1 2 3 4
5 6 7 8
9 10 11 12
13 14 15 16
我用的是两个一维数组分别存对应编号格子的内容和是否合并处理过(合并处理在我写的changetd()
函数中,具体见下一部分)
在4*4的表格中我是分成了4列(或行,上下是列,左右是行)读入,读入顺序是正序或倒序(我是上左倒,下右正,这主要是方便changetd()
函数统一操作),然后按此顺序存入上述的数组中。
在changetd()
函数处理完后再更新对应编号的格子显示的内容
该部分代码如下
//上下左右操作函数 //向上操作 function Up() { for(var i=1;i<=mapy;i++){ var tempmap=[];//记录该编号格子内的数 var tempflag=[];//记录该编号格子是否合并过(本次操作内) var z=0; for(var j=i+(mapx-1)*mapy;j>=i;j-=mapy){ var thetd=document.getElementById(j); if(thetd.innerHTML==""){ tempmap[z]=0; } else{ tempmap[z]=parseInt(thetd.innerHTML); } tempflag[z]=true; z++; } //数字合并统一由changetd函数完成 tempmap=changetd(tempmap,tempflag,tempmap.length,0); z=0; //更新格子内的显示内容 for(var j=i+(mapx-1)*mapy;j>=i;j-=mapy){ var thetd=document.getElementById(j); if(tempmap[z]==0){ thetd.innerHTML=""; } else{ thetd.innerHTML=tempmap[z]; } z++; } } } //向下操作 function Down() { for(var i=1;i<=mapy;i++){ var tempmap=[]; var tempflag=[]; var z=0; for(var j=i;j<=i+(mapx-1)*mapy;j+=mapy){ var thetd=document.getElementById(j); if(thetd.innerHTML==""){ tempmap[z]=0; } else{ tempmap[z]=parseInt(thetd.innerHTML); } tempflag[z]=true; z++; } tempmap=changetd(tempmap,tempflag,tempmap.length,0); z=0; for(var j=i;j<=i+(mapx-1)*mapy;j+=mapy){ var thetd=document.getElementById(j); if(tempmap[z]==0){ thetd.innerHTML=""; } else{ thetd.innerHTML=tempmap[z]; } z++; } } } //向左操作 function Left() { for(var i=mapy;i<=mapy+(mapx-1)*mapy;i+=mapy){ var tempmap=[]; var tempflag=[]; var z=0; for(var j=i;j>=i-mapy+1;j--){ var thetd=document.getElementById(j); if(thetd.innerHTML==""){ tempmap[z]=0; } else{ tempmap[z]=parseInt(thetd.innerHTML); } tempflag[z]=true; z++; } tempmap=changetd(tempmap,tempflag,tempmap.length,0); z=0; for(var j=i;j>=i-mapy+1;j--){ var thetd=document.getElementById(j); if(tempmap[z]==0){ thetd.innerHTML=""; } else{ thetd.innerHTML=tempmap[z]; } z++; } } } //向右操作 function Right() { for(var i=1;i<=1+(mapx-1)*mapy;i+=mapy){ var tempmap=[]; var tempflag=[]; var z=0; console.log(i+" i"); for(var j=i;j<i+mapy;j++){ console.log(j); var thetd=document.getElementById(j); if(thetd.innerHTML==""){ tempmap[z]=0; } else{ tempmap[z]=parseInt(thetd.innerHTML); } tempflag[z]=true; z++; } tempmap=changetd(tempmap,tempflag,tempmap.length,0); z=0; for(var j=i;j<i+mapy;j++){ var thetd=document.getElementById(j); if(tempmap[z]==0){ thetd.innerHTML=""; } else{ thetd.innerHTML=tempmap[z]; } z++; } } }
5.合并数字
上文提到的changetd()函数来实现这部分内容
代码如下
function changetd(tempmap, tempflag, k, u) { for (var i = k - 1; i > u; i--) { if (tempmap[i - 1] != 0 && tempmap[i] == 0) { //移动 tempmap[i] = tempmap[i - 1]; tempmap[i - 1] = 0; //移动时合并标记也同样跟随移动 if (tempflag[i - 1] == false) { tempflag[i - 1] = true; tempflag[i] = false; } flag_r = true; } else if (tempmap[i - 1] != 0 && tempmap[i] == tempmap[i - 1] && tempflag[i] == true && tempflag[i - 1] == true) { //合并 tempmap[i] *= 2; tempmap[i - 1] = 0; //标记经过合并处理 tempflag[i] = false; flag_r = true; mark.innerHTML = parseInt(mark.innerHTML) + tempmap[i]; } //递归,以解决可能出现的合并后产生空位未处理 tempmap = changetd(tempmap, tempflag, k, i); } return tempmap; }
这部分首先解决的是判断某个数是否在本轮操作中合并过,这问题的解决我是通过另一个数再记录对应是否合并的状态,具体见上面代码、
然后通过递归来解决一次遍历操作过后,可能留下的空位的问题
6.颜色更改
为了美观以及更好的体现数值的不同,根据不同数值给格子渲染上不同背景色
function tdcolor() { //根据不同数值给格子渲染上不同背景色 var tdcolors = { "": "#cdc1b4", "2": "#eee4da", "4": "#ede0c8", "8": "#f2b179", "16": "#f59563", "32": "#f67c5f", "64": "#f65e3b", "128": "#edcf72", "256": "#edcc61", "512": "#9c0", "1024": "#33b5e5", "2048": "#09c", "4096": "#a6c", "8192": "#93c" } //数字颜色更改 for (var i = 1; i <= mapx * mapy; i++) { var thetd = document.getElementById(i); thetd.style.backgroundColor = tdcolors[thetd.innerHTML]; if (thetd.innerHTML == 2 || thetd.innerHTML == 4) { thetd.style.color = "#776e65"; } else { thetd.style.color = "#f8f5f1"; } } }
这部分我担心我自己配色会配的乱七八糟,所以我根据原版的2048配色来?
当格子里的数字大于4时数字颜色变白的情况我也顺便还原了
另外我还设置了大于2048的值?
游戏结束或达到胜利值时提示
游戏结束或达到胜利值时需要有所提示
function isover() { var f = 0; for (var i = 1; i <= mapx * mapy; i++) { var td = document.getElementById(i); if(td.innerHTML >= youwin){ document.getElementById("gameover").innerHTML="恭喜你达到了 "+td.innerHTML; document.getElementById("gameover").style.display = "block"; youwin=parseInt(td.innerHTML); } if (td.innerHTML == "") { //空值跳过 } else if (i <= (mapx - 1) * mapy && td.innerHTML == document.getElementById(i + mapy).innerHTML) { //判断该格子下方的数是否与之相同 } else if (i % mapy != 0 && td.innerHTML == document.getElementById(i + 1).innerHTML) { //判断该格子右边的数是否与之相同 } else { f++; } } if (f == mapx * mapy) { document.getElementById("gameover").innerHTML+="<br>GAME OVER" document.getElementById("gameover").style.display = "block"; overflag = false; } }
这部分代码逻辑具体见注释?
youwin
的值在初始的那一部分中main()函数中设置
js部分总览
综上,js部分总体代码如下
window.onload = main(); //加载主函数 var overflag = true; //判断游戏是否结束的标志 //主函数 function main() { mapx = 4, mapy = 4, mapt = mapx * mapy; //设定表框大小,一般为4*4 var table = document.getElementById("maintable"); //获取表格 var tableStructure = ""; //储存表格结构 var tdid = 1; //单元格id var mark = document.getElementById("mark"); //获取html中显示分数的位置 overflag = true; //因为后续新游戏是重新执行该函数,所以该值要重新变为true //加载表格结构(4行4列) for (var i = 1; i <= mapx; i++) { tableStructure += "<tr>"; //为表格加1行 //为该行添加单元格 for (var j = 1; j <= mapy; j++) { tableStructure += "<td id=" + tdid + "></td>"; //添加单元格与该单元格id tdid++; } tableStructure += "</tr>"; } table.innerHTML = tableStructure; //向表格返回该表格结构 tdRandom(); tdRandom(); //随机两个格子给予2或 4 tdcolor(); //渲染格子的颜色 mark.innerHTML = 0; //初始总分为0 document.getElementById("gameover").style.display = "none"; //初始GME OVER提示不显示 document.getElementById("gameover").innerHTML=""; youwin=2048; } //因为Math.random() 返回 0(包括) 至 1(不包括) 之间的随机数,随机的数不一定是整数。这里我用向下取整 function myrandom(min, max) { return min + Math.floor(Math.random() * max); } //随机为单元格随机填入2或4 (4出现的概率应相对较小) function tdRandom() { var temp = myrandom(1, mapt); if (document.getElementById(temp).innerHTML == "") { document.getElementById(temp).innerHTML = Math.random() < 0.9 ? 2 : 4; } else { tdRandom(); } } //键盘事件响应 function keyboardEvents() { //以键盘方向键或wasd操作移动 if (overflag) { if (event.keyCode == 37 || event.keyCode == 65) Left(); else if (event.keyCode == 38 || event.keyCode == 87) Up(); else if (event.keyCode == 39 || event.keyCode == 68) Right(); else if (event.keyCode == 40 || event.keyCode == 83) Down(); //保证其它按键不会触发 if (flag_r) { tdRandom(); //随机再产生一个数字 flag_r = false; } } tdcolor(); //更新格子颜色 if(overflag) isover(); //判断游戏是否结束 } //上下左右操作函数 //向上操作 function Up() { for (var i = 1; i <= mapy; i++) { var tempmap = []; //记录该编号格子内的数 var tempflag = []; //记录该编号格子是否合并过(本次操作内) var z = 0; for (var j = i + (mapx - 1) * mapy; j >= i; j -= mapy) { var thetd = document.getElementById(j); if (thetd.innerHTML == "") { tempmap[z] = 0; } else { tempmap[z] = parseInt(thetd.innerHTML); } tempflag[z] = true; z++; } //数字合并统一由changetd函数完成 tempmap = changetd(tempmap, tempflag, tempmap.length, 0); z = 0; //更新格子内的显示内容 for (var j = i + (mapx - 1) * mapy; j >= i; j -= mapy) { var thetd = document.getElementById(j); if (tempmap[z] == 0) { thetd.innerHTML = ""; } else { thetd.innerHTML = tempmap[z]; } z++; } } } //向下操作 function Down() { for (var i = 1; i <= mapy; i++) { var tempmap = []; var tempflag = []; var z = 0; for (var j = i; j <= i + (mapx - 1) * mapy; j += mapy) { var thetd = document.getElementById(j); if (thetd.innerHTML == "") { tempmap[z] = 0; } else { tempmap[z] = parseInt(thetd.innerHTML); } tempflag[z] = true; z++; } tempmap = changetd(tempmap, tempflag, tempmap.length, 0); z = 0; for (var j = i; j <= i + (mapx - 1) * mapy; j += mapy) { var thetd = document.getElementById(j); if (tempmap[z] == 0) { thetd.innerHTML = ""; } else { thetd.innerHTML = tempmap[z]; } z++; } } } //向左操作 function Left() { for (var i = mapy; i <= mapy + (mapx - 1) * mapy; i += mapy) { var tempmap = []; var tempflag = []; var z = 0; for (var j = i; j >= i - mapy + 1; j--) { var thetd = document.getElementById(j); if (thetd.innerHTML == "") { tempmap[z] = 0; } else { tempmap[z] = parseInt(thetd.innerHTML); } tempflag[z] = true; z++; } tempmap = changetd(tempmap, tempflag, tempmap.length, 0); z = 0; for (var j = i; j >= i - mapy + 1; j--) { var thetd = document.getElementById(j); if (tempmap[z] == 0) { thetd.innerHTML = ""; } else { thetd.innerHTML = tempmap[z]; } z++; } } } //向右操作 function Right() { for (var i = 1; i <= 1 + (mapx - 1) * mapy; i += mapy) { var tempmap = []; var tempflag = []; var z = 0; console.log(i + " i"); for (var j = i; j < i + mapy; j++) { console.log(j); var thetd = document.getElementById(j); if (thetd.innerHTML == "") { tempmap[z] = 0; } else { tempmap[z] = parseInt(thetd.innerHTML); } tempflag[z] = true; z++; } tempmap = changetd(tempmap, tempflag, tempmap.length, 0); z = 0; for (var j = i; j < i + mapy; j++) { var thetd = document.getElementById(j); if (tempmap[z] == 0) { thetd.innerHTML = ""; } else { thetd.innerHTML = tempmap[z]; } z++; } } } function changetd(tempmap, tempflag, k, u) { for (var i = k - 1; i > u; i--) { if (tempmap[i - 1] != 0 && tempmap[i] == 0) { //移动 tempmap[i] = tempmap[i - 1]; tempmap[i - 1] = 0; //移动时合并标记也同样跟随移动 if (tempflag[i - 1] == false) { tempflag[i - 1] = true; tempflag[i] = false; } flag_r = true; } else if (tempmap[i - 1] != 0 && tempmap[i] == tempmap[i - 1] && tempflag[i] == true && tempflag[i - 1] == true) { //合并 tempmap[i] *= 2; tempmap[i - 1] = 0; //标记经过合并处理 tempflag[i] = false; flag_r = true; mark.innerHTML = parseInt(mark.innerHTML) + tempmap[i]; } //递归,以解决可能出现的合并后产生空位未处理 tempmap = changetd(tempmap, tempflag, k, i); } return tempmap; } function tdcolor() { //根据不同数值给格子渲染上不同背景色 var tdcolors = { "": "#cdc1b4", "2": "#eee4da", "4": "#ede0c8", "8": "#f2b179", "16": "#f59563", "32": "#f67c5f", "64": "#f65e3b", "128": "#edcf72", "256": "#edcc61", "512": "#9c0", "1024": "#33b5e5", "2048": "#09c", "4096": "#a6c", "8192": "#93c" } //数字颜色更改 for (var i = 1; i <= mapx * mapy; i++) { var thetd = document.getElementById(i); thetd.style.backgroundColor = tdcolors[thetd.innerHTML]; if (thetd.innerHTML == 2 || thetd.innerHTML == 4) { thetd.style.color = "#776e65"; } else { thetd.style.color = "#f8f5f1"; } } } function isover() { var f = 0; for (var i = 1; i <= mapx * mapy; i++) { var td = document.getElementById(i); if(td.innerHTML >= youwin){ document.getElementById("gameover").innerHTML="恭喜你达到了 "+td.innerHTML; document.getElementById("gameover").style.display = "block"; youwin=parseInt(td.innerHTML); } if (td.innerHTML == "") { //空值跳过 } else if (i <= (mapx - 1) * mapy && td.innerHTML == document.getElementById(i + mapy).innerHTML) { //判断该格子下方的数是否与之相同 } else if (i % mapy != 0 && td.innerHTML == document.getElementById(i + 1).innerHTML) { //判断该格子右边的数是否与之相同 } else { f++; } } if (f == mapx * mapy) { document.getElementById("gameover").innerHTML+="<br>GAME OVER" document.getElementById("gameover").style.display = "block"; overflag = false; } }
后记
限于我个人糟糕的水平,上面的内容可能有许多不完善的地方或者有更好的办法,欢迎指出
也欢迎访问我的小破站https://www.226yzy.com/或者https://226yzy.github.io/
我的Github226YZY (星空下的YZY) (github.com)