├── 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 | 
22 |
23 | ## 5、游戏实现原理
24 |
25 | ### (1)首先,把16宫格看成是矩阵的形式
26 |
27 | 
28 |
29 | ### (2)在html中给每个格子添加类名及属性,来记录每个格子的位置
30 |
31 | 
32 |
33 | 注:类名`item`是每个格子的类名,`emptyItem`是空格子的类名,`nonEmptyItem`是非空格子的类名。
34 |
35 | ### (3)游戏开始时,随机生成两个数字,2或者4,出现在矩阵中任意位置
36 |
37 | 
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 |
49 |
电脑:请用键盘的方向键进行操作
50 |
手机:请划动屏幕进行操作
51 |
52 |
53 |
得分:0 分
54 |
57 |
58 |
62 |
63 |
65 |
66 |
67 |
73 |
74 | Game Over!
75 |
76 |
80 |
81 |
82 |
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 | });
--------------------------------------------------------------------------------