├── main.css ├── README.md ├── index.html └── main.js /main.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | font-family: "YouYuan"; 5 | } 6 | 7 | body { 8 | background: #d7d3b6; 9 | } 10 | 11 | .container { 12 | /*padding-top: 30px;*/ 13 | width: 100%; 14 | height: 100%; 15 | } 16 | 17 | .main { 18 | width: 100%; 19 | height: 100%; 20 | margin: 0 auto; 21 | overflow: hidden; 22 | text-align: center; 23 | } 24 | 25 | .main .gameName { 26 | font-size: 35px; 27 | font-weight: bold; 28 | } 29 | 30 | .main .maxScore { 31 | font-size: 20px; 32 | } 33 | 34 | .main .maxScore span { 35 | color: red; 36 | font-weight: bold; 37 | } 38 | 39 | .main .gameBody { 40 | /*width: 100%;*/ 41 | height: 50%; 42 | margin: 0 auto; 43 | display: flex; 44 | flex-direction: column; 45 | justify-content: space-between; 46 | padding: 15px; 47 | background: #999; 48 | border-radius: 8px; 49 | padding-top: 5px; 50 | padding-bottom: 5px; 51 | } 52 | 53 | .main .gameBody .row { 54 | display: flex; 55 | justify-content: space-between; 56 | } 57 | 58 | .main .gameBody .row .item { 59 | width: 70px; 60 | height: 70px; 61 | border-radius: 10px; 62 | background: #fff; 63 | text-align: center; 64 | line-height: 70px; 65 | font-size: 30px; 66 | font-weight: bold; 67 | margin: 5px; 68 | color: #666; 69 | font-family: "microsoft yahei"; 70 | } 71 | 72 | .main .gameRule { 73 | font-size: 16px; 74 | font-weight: bold; 75 | margin-top: 5px; 76 | } 77 | 78 | .main .gameScore { 79 | font-size: 20px; 80 | font-weight: bold; 81 | margin-top: 0px; 82 | } 83 | 84 | .main .gameScore span { 85 | color: red; 86 | font-size: 30px; 87 | } 88 | 89 | .main .scoreAndRefresh { 90 | display: flex; 91 | justify-content: space-around; 92 | width: 280px; 93 | margin: 0 auto; 94 | } 95 | 96 | .main .scoreAndRefresh .refreshBtn { 97 | height: 30px; 98 | margin-top: 8px; 99 | } 100 | 101 | .modal { 102 | margin-top: 150px; 103 | 104 | } 105 | 106 | .modal .modal-header h4 { 107 | text-align: left; 108 | font-weight: bold; 109 | } 110 | 111 | .modal .modal-dialog { 112 | width: 300px; 113 | margin: 0 auto; 114 | 115 | } 116 | 117 | .modal .modal-body { 118 | font-size: 18px; 119 | font-weight: bold; 120 | color: red; 121 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # js实现2048小游戏 🎮 2 | 3 | 演示地址:[https://nnngu.github.io/js_game_2048/index.html](https://nnngu.github.io/js_game_2048/index.html) 4 | 5 | ## 1、游戏简介 6 | 7 | 2048是一款休闲益智类的数字叠加小游戏。 8 | 9 | ## 2、游戏玩法 10 | 11 | 在 4*4 的16宫格中,您可以选择上、下、左、右四个方向进行操作,数字会按方向移动,相邻的两个数字相同就会合并,组成更大的数字,每次移动或合并后会自动增加一个数字。 12 | 13 | 当16宫格中没有空格子,且四个方向都无法操作时,游戏结束。 14 | 15 | ## 3、游戏目的 16 | 17 | 目的是合并出 2048 这个数字,获得更高的分数。 18 | 19 | ## 4、游戏截图 20 | 21 | ![](https://raw.githubusercontent.com/nnngu/FigureBed/master/2018/2/9/Snip20180210_7.png) 22 | 23 | ## 5、游戏实现原理 24 | 25 | ### (1)首先,把16宫格看成是矩阵的形式 26 | 27 | ![](https://raw.githubusercontent.com/nnngu/FigureBed/master/2018/2/9/Snip20180209_2.png) 28 | 29 | ### (2)在html中给每个格子添加类名及属性,来记录每个格子的位置 30 | 31 | ![](https://raw.githubusercontent.com/nnngu/FigureBed/master/2018/2/9/Snip20180209_4.png) 32 | 33 | 注:类名`item`是每个格子的类名,`emptyItem`是空格子的类名,`nonEmptyItem`是非空格子的类名。 34 | 35 | ### (3)游戏开始时,随机生成两个数字,2或者4,出现在矩阵中任意位置 36 | 37 | ![](https://raw.githubusercontent.com/nnngu/FigureBed/master/2018/2/9/Snip20180209_5.png) 38 | 39 | 这部分是通过类名`emptyItem`及`nonEmptyItem`来实现的。 40 | 41 | 步骤: 42 | 43 | ① 随机生成一个数字2或者4 44 | 45 | ② 获取所有空元素(类名`emptyItem`) 46 | 47 | ③ 随机选择一个空元素,将生成的数字填充到空元素中,并将类名`emptyItem`移除,添加类名`nonEmptyItem`,即非空元素 48 | 49 | ④ 重复①、②、③步,再随机生成一个数字填充到随机的位置。 50 | 51 | ### (4)游戏的核心在于移动 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 | ### (5)判断游戏是否结束 88 | 89 | ``` 90 | 获取所有元素 91 | 获取所有非空元素 92 | 如果所有元素的个数 == 所有非空元素的个数 93 |   循环遍历所有非空元素 94 |     上面元素存在 && (当前元素的内容 == 上面元素的内容) return 95 |     下面元素存在 && (当前元素的内容 == 下面元素的内容) return 96 |     左边元素存在 && (当前元素的内容 == 左边元素的内容) return 97 |     右边元素存在 && (当前元素的内容 == 右边元素的内容) return 98 |   以上条件都不满足,Game Over! 99 | ``` 100 | 101 | 源代码:[https://github.com/nnngu/js_game_2048](https://github.com/nnngu/js_game_2048) 102 | 103 | 演示地址:[https://nnngu.github.io/js_game_2048/index.html](https://nnngu.github.io/js_game_2048/index.html) 104 | 105 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 2048小游戏 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 |
2048小游戏
19 |
最高分: 20 | 1345612 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 |
得分:0
54 | 57 |
58 | 62 | 63 | 83 |
84 |
85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | //是否产生新元素 3 | var isNewRndItem = false; 4 | var gameScore = 0; 5 | //最高分 6 | var maxScore = 0; 7 | 8 | if (localStorage.maxScore) { 9 | maxScore = localStorage.maxScore - 0; 10 | } else { 11 | maxScore = 0; 12 | } 13 | 14 | //游戏初始化 15 | gameInit(); 16 | 17 | function refreshGame() { 18 | var items = $('.gameBody .row .item'); 19 | for (var i = 0; i < items.length; i++) { 20 | items.eq(i).html('').removeClass('nonEmptyItem').addClass('emptyItem'); 21 | } 22 | gameScore = 0; 23 | //分数清零 24 | $('#gameScore').html(gameScore); 25 | //随机生成两个新元素 26 | newRndItem(); 27 | newRndItem(); 28 | //刷新颜色 29 | refreshColor(); 30 | $('#gameOverModal').modal('hide'); 31 | } 32 | 33 | 34 | function getSideItem(currentItem, direction) { 35 | //当前元素的位置 36 | var currentItemX = currentItem.attr('x') - 0; 37 | var currentItemY = currentItem.attr('y') - 0; 38 | 39 | //根据方向获取旁边元素的位置 40 | switch (direction) { 41 | case 'left': 42 | var sideItemX = currentItemX; 43 | var sideItemY = currentItemY - 1; 44 | break; 45 | case 'right': 46 | var sideItemX = currentItemX; 47 | var sideItemY = currentItemY + 1; 48 | break; 49 | case 'up': 50 | var sideItemX = currentItemX - 1; 51 | var sideItemY = currentItemY; 52 | break; 53 | case 'down': 54 | var sideItemX = currentItemX + 1; 55 | var sideItemY = currentItemY; 56 | break; 57 | } 58 | //旁边元素 59 | var sideItem = $('.gameBody .row .x' + sideItemX + 'y' + sideItemY); 60 | return sideItem; 61 | } 62 | 63 | 64 | function itemMove(currentItem, direction) { 65 | 66 | var sideItem = getSideItem(currentItem, direction); 67 | 68 | if (sideItem.length == 0) {//当前元素在最边上 69 | //不动 70 | 71 | } else if (sideItem.html() == '') { //当前元素不在最后一个且左(右、上、下)侧元素是空元素 72 | sideItem.html(currentItem.html()).removeClass('emptyItem').addClass('nonEmptyItem'); 73 | currentItem.html('').removeClass('nonEmptyItem').addClass('emptyItem'); 74 | itemMove(sideItem, direction); 75 | isNewRndItem = true; 76 | 77 | } else if (sideItem.html() != currentItem.html()) {//左(右、上、下)侧元素和当前元素内容不同 78 | //不动 79 | 80 | } else {//左(右、上、下)侧元素和当前元素内容相同 81 | //向右合并 82 | sideItem.html((sideItem.html() - 0) * 2); 83 | currentItem.html('').removeClass('nonEmptyItem').addClass('emptyItem'); 84 | gameScore += (sideItem.text() - 0) * 10; 85 | $('#gameScore').html(gameScore); 86 | // itemMove(sideItem, direction); 87 | maxScore = maxScore < gameScore ? gameScore : maxScore; 88 | $('#maxScore').html(maxScore); 89 | localStorage.maxScore = maxScore; 90 | isNewRndItem = true; 91 | return; 92 | } 93 | } 94 | 95 | 96 | function move(direction) { 97 | //获取所有非空元素 98 | var nonEmptyItems = $('.gameBody .row .nonEmptyItem'); 99 | //如果按下的方向是左或上,则正向遍历非空元素 100 | if (direction == 'left' || direction == 'up') { 101 | for (var i = 0; i < nonEmptyItems.length; i++) { 102 | var currentItem = nonEmptyItems.eq(i); 103 | itemMove(currentItem, direction); 104 | } 105 | } else if (direction == 'right' || direction == 'down') {//如果按下的方向是右或下,则反向遍历非空元素 106 | for (var i = nonEmptyItems.length - 1; i >= 0; i--) { 107 | var currentItem = nonEmptyItems.eq(i); 108 | itemMove(currentItem, direction); 109 | } 110 | } 111 | 112 | //是否产生新元素 113 | if (isNewRndItem) { 114 | newRndItem(); 115 | refreshColor(); 116 | } 117 | } 118 | 119 | function isGameOver() { 120 | //获取所有元素 121 | var items = $('.gameBody .row .item'); 122 | //获取所有非空元素 123 | var nonEmptyItems = $('.gameBody .row .nonEmptyItem'); 124 | if (items.length == nonEmptyItems.length) {//所有元素的个数 == 所有非空元素的个数 即没有空元素 125 | //遍历所有非空元素 126 | for (var i = 0; i < nonEmptyItems.length; i++) { 127 | var currentItem = nonEmptyItems.eq(i); 128 | if (getSideItem(currentItem, 'up').length != 0 && currentItem.html() == getSideItem(currentItem, 'up').html()) { 129 | //上边元素存在 且 当前元素中的内容等于上边元素中的内容 130 | return; 131 | } else if (getSideItem(currentItem, 'down').length != 0 && currentItem.html() == getSideItem(currentItem, 'down').html()) { 132 | //下边元素存在 且 当前元素中的内容等于下边元素中的内容 133 | return; 134 | } else if (getSideItem(currentItem, 'left').length != 0 && currentItem.html() == getSideItem(currentItem, 'left').html()) { 135 | //左边元素存在 且 当前元素中的内容等于左边元素中的内容 136 | return; 137 | } else if (getSideItem(currentItem, 'right').length != 0 && currentItem.html() == getSideItem(currentItem, 'right').html()) { 138 | //右边元素存在 且 当前元素中的内容等于右边元素中的内容 139 | return; 140 | } 141 | } 142 | } else { 143 | return; 144 | } 145 | $('#gameOverModal').modal('show'); 146 | } 147 | 148 | //游戏初始化 149 | function gameInit() { 150 | //初始化分数 151 | $('#gameScore').html(gameScore); 152 | //最大分值 153 | $('#maxScore').html(maxScore); 154 | //为刷新按钮绑定事件 155 | $('.refreshBtn').click(refreshGame); 156 | //随机生成两个新元素 157 | newRndItem(); 158 | newRndItem(); 159 | //刷新颜色 160 | refreshColor(); 161 | } 162 | 163 | //随机生成新元素 164 | function newRndItem() { 165 | //随机生成新数字 166 | var newRndArr = [2, 2, 4]; 167 | var newRndNum = newRndArr[getRandom(0, 2)]; 168 | console.log('newRndNum: ' + newRndNum); 169 | //随机生成新数字的位置 170 | var emptyItems = $('.gameBody .row .emptyItem'); 171 | var newRndSite = getRandom(0, emptyItems.length - 1); 172 | emptyItems.eq(newRndSite).html(newRndNum).removeClass('emptyItem').addClass('nonEmptyItem'); 173 | } 174 | 175 | //产生随机数,包括min、max 176 | function getRandom(min, max) { 177 | return min + Math.floor(Math.random() * (max - min + 1)); 178 | } 179 | 180 | //刷新颜色 181 | function refreshColor() { 182 | var items = $('.gameBody .item'); 183 | for (var i = 0; i < items.length; i++) { 184 | // console.log(items.eq(i).parent().index()); 185 | switch (items.eq(i).html()) { 186 | case '': 187 | items.eq(i).css('background', ''); 188 | break; 189 | case '2': 190 | items.eq(i).css('background', 'rgb(250, 225, 188)'); 191 | break; 192 | case '4': 193 | items.eq(i).css('background', 'rgb(202, 240, 240)'); 194 | break; 195 | case '8': 196 | items.eq(i).css('background', 'rgb(117, 231, 193)'); 197 | break; 198 | case '16': 199 | items.eq(i).css('background', 'rgb(240, 132, 132)'); 200 | break; 201 | case '32': 202 | items.eq(i).css('background', 'rgb(181, 240, 181)'); 203 | break; 204 | case '64': 205 | items.eq(i).css('background', 'rgb(182, 210, 246)'); 206 | break; 207 | case '128': 208 | items.eq(i).css('background', 'rgb(255, 207, 126)'); 209 | break; 210 | case '256': 211 | items.eq(i).css('background', 'rgb(250, 216, 216)'); 212 | break; 213 | case '512': 214 | items.eq(i).css('background', 'rgb(124, 183, 231)'); 215 | break; 216 | case '1024': 217 | items.eq(i).css('background', 'rgb(225, 219, 215)'); 218 | break; 219 | case '2048': 220 | items.eq(i).css('background', 'rgb(221, 160, 221)'); 221 | break; 222 | case '4096': 223 | items.eq(i).css('background', 'rgb(250, 139, 176)'); 224 | break; 225 | } 226 | } 227 | } 228 | 229 | // 电脑的方向键监听事件 230 | $('body').keydown(function (e) { 231 | switch (e.keyCode) { 232 | case 37: 233 | // left 234 | console.log('left'); 235 | isNewRndItem = false; 236 | move('left'); 237 | isGameOver(); 238 | break; 239 | case 38: 240 | // up 241 | console.log('up'); 242 | isNewRndItem = false; 243 | move('up'); 244 | isGameOver(); 245 | break; 246 | case 39: 247 | // right 248 | console.log('right'); 249 | isNewRndItem = false; 250 | move('right'); 251 | isGameOver(); 252 | break; 253 | case 40: 254 | // down 255 | console.log('down'); 256 | isNewRndItem = false; 257 | move('down'); 258 | isGameOver(); 259 | break; 260 | } 261 | }); 262 | 263 | // 手机屏幕划动触发 264 | (function () { 265 | mobilwmtouch(document.getElementById("gameBody")) 266 | document.getElementById("gameBody").addEventListener('touright', function (e) { 267 | e.preventDefault(); 268 | // alert("方向向右"); 269 | console.log('right'); 270 | isNewRndItem = false; 271 | move('right'); 272 | isGameOver(); 273 | }); 274 | document.getElementById("gameBody").addEventListener('touleft', function (e) { 275 | // alert("方向向左"); 276 | console.log('left'); 277 | isNewRndItem = false; 278 | move('left'); 279 | isGameOver(); 280 | }); 281 | document.getElementById("gameBody").addEventListener('toudown', function (e) { 282 | // alert("方向向下"); 283 | console.log('down'); 284 | isNewRndItem = false; 285 | move('down'); 286 | isGameOver(); 287 | }); 288 | document.getElementById("gameBody").addEventListener('touup', function (e) { 289 | // alert("方向向上"); 290 | console.log('up'); 291 | isNewRndItem = false; 292 | move('up'); 293 | isGameOver(); 294 | }); 295 | 296 | function mobilwmtouch(obj) { 297 | var stoux, stouy; 298 | var etoux, etouy; 299 | var xdire, ydire; 300 | obj.addEventListener("touchstart", function (e) { 301 | stoux = e.targetTouches[0].clientX; 302 | stouy = e.targetTouches[0].clientY; 303 | //console.log(stoux); 304 | }, false); 305 | obj.addEventListener("touchend", function (e) { 306 | etoux = e.changedTouches[0].clientX; 307 | etouy = e.changedTouches[0].clientY; 308 | xdire = etoux - stoux; 309 | ydire = etouy - stouy; 310 | chazhi = Math.abs(xdire) - Math.abs(ydire); 311 | //console.log(ydire); 312 | if (xdire > 0 && chazhi > 0) { 313 | console.log("right"); 314 | //alert(evenzc('touright',alerts)); 315 | obj.dispatchEvent(evenzc('touright')); 316 | 317 | } else if (ydire > 0 && chazhi < 0) { 318 | console.log("down"); 319 | obj.dispatchEvent(evenzc('toudown')); 320 | } else if (xdire < 0 && chazhi > 0) { 321 | console.log("left"); 322 | obj.dispatchEvent(evenzc('touleft')); 323 | } else if (ydire < 0 && chazhi < 0) { 324 | console.log("up"); 325 | obj.dispatchEvent(evenzc('touup')); 326 | } 327 | }, false); 328 | 329 | function evenzc(eve) { 330 | if (typeof document.CustomEvent === 'function') { 331 | 332 | this.event = new document.CustomEvent(eve, {//自定义事件名称 333 | bubbles: false,//是否冒泡 334 | cancelable: false//是否可以停止捕获 335 | }); 336 | if (!document["evetself" + eve]) { 337 | document["evetself" + eve] = this.event; 338 | } 339 | } else if (typeof document.createEvent === 'function') { 340 | 341 | 342 | this.event = document.createEvent('HTMLEvents'); 343 | this.event.initEvent(eve, false, false); 344 | if (!document["evetself" + eve]) { 345 | document["evetself" + eve] = this.event; 346 | } 347 | } else { 348 | return false; 349 | } 350 | 351 | return document["evetself" + eve]; 352 | 353 | } 354 | } 355 | })(); 356 | }); --------------------------------------------------------------------------------