前言
注:该篇记录暂未实现过渡动画以及移动端的上下左右操作
我的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在这方面有些问题,下面列出我注意到的情况
2 2 2 2这行如果向左移动后结果应为4 4 0 0
也就是说,合并后的数字不与接下来相同的数字再合并
例2(合并顺序)
2 2 2 4右移后应为0 2 4 4
说明向右移,合并应从右侧开始
若为左移,则合并应从左侧开始,上下移动的同理
5.上下左右操作响应
对键盘事件响应,通过wsad或方向键对应上下左右操作
6.游戏是否结束或达到胜利值
在每次操作后,随即遍历每个格子上下左右是否还有相同的数字
若存在某个格子上下左右中存在与之相同的数字,那么游戏尚未结束
否则,若所有格子上下左右中都不存在与之相同的数字,则说明游戏结束
胜利值在上面遍历时查找即可
代码实现
HTML部分
html部分总览
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| <!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> <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> <table id="maintable" cellspacing="10"></table> <div id="gameover"></div> </body>
<script src="js/main2048.js"></script> </html>
|
当前总分显示与新游戏按钮
该部分代码如下
1 2 3 4
| <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游戏主体
1 2
| <table id="maintable" cellspacing="10"></table>
|
这部分我用table
实现,里面的td
通过js渲染上去,渲染后如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| <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
实现的,你也可以用这种做法
游戏结束或达到胜利值时提示
1 2
| <div id="gameover"></div>
|
该部分通过css使其初始不显示,当判断游戏结束或达到胜利值时由js更改其style和内容,使其显示
CSS部分
css部分总览
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| body { text-align: center; background-color: #E0FFFF; }
table { min-width: 340px; font-size: 30px; font-weight: bold; background-color: #bbada0; margin: 0px auto; 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.初始
各种页面渲染及各种函数调用都基本在这一部分实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| window.onload = main(); var overflag=true;
function main() { mapx=4,mapy=4,mapt=mapx*mapy; var table = document.getElementById("maintable"); var tableStructure = ""; var tdid = 1; var mark=document.getElementById("mark"); overflag=true;
for (var i = 1; i <= mapx; i++) { tableStructure += "<tr>"; for (var j = 1; j <= mapy; j++) { tableStructure += "<td id=" + tdid + "></td>"; tdid++; } tableStructure += "</tr>"; } table.innerHTML = tableStructure; tdRandom(); tdRandom(); tdcolor(); mark.innerHTML=0; document.getElementById("gameover").style.display="none"; document.getElementById("gameover").innerHTML=""; youwin=2048; }
|
原版2048为4*4的表格,我在代码中的mapx,mapy的值可以自行设置,以便魔改成更大的表格供游玩。
youwin
的值代表胜利值可自行修改
2.随机相关
这部分需要解决随机格子随机产生2或4
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| function myrandom(min, max) { return min + Math.floor(Math.random() * max); }
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.键盘事件响应
注:
由于所学有限,本篇的移动操作暂时只实现了键盘事件的响应,移动端的触屏操作待后续研究
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| function keyboardEvents() { 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()
函数处理完后再更新对应编号的格子显示的内容
该部分代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134
|
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++; } 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()函数来实现这部分内容
代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| 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.颜色更改
为了美观以及更好的体现数值的不同,根据不同数值给格子渲染上不同背景色
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| 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的值🤪
游戏结束或达到胜利值时提示
游戏结束或达到胜利值时需要有所提示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| 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部分总体代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276
| window.onload = main(); var overflag = true;
function main() { mapx = 4, mapy = 4, mapt = mapx * mapy; var table = document.getElementById("maintable"); var tableStructure = ""; var tdid = 1; var mark = document.getElementById("mark"); overflag = true;
for (var i = 1; i <= mapx; i++) { tableStructure += "<tr>"; for (var j = 1; j <= mapy; j++) { tableStructure += "<td id=" + tdid + "></td>"; tdid++; } tableStructure += "</tr>"; } table.innerHTML = tableStructure; tdRandom(); tdRandom(); tdcolor(); mark.innerHTML = 0; document.getElementById("gameover").style.display = "none"; document.getElementById("gameover").innerHTML=""; youwin=2048; }
function myrandom(min, max) { return min + Math.floor(Math.random() * max); }
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() { 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++; } 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)