经过一段时间学习,对javascript有了一个初步的了解自己制作了一个扫雷,源代码+详细注释放在后面,先看下效果图。
初始化界面:
游戏界面:
难易程度切换:
游戏结束:
思路
采用构造函数的形式进行全局开发
生成游戏棋盘
- 利用双层for循环创建设定的棋盘大小
- 为每个单元格的dom元素创建一个属性,该属性用于保存单元格的所有信息,如x,y坐标,value,是否为雷等
随机生成炸弹
- 利用随机数,随机生成炸弹x,y坐标,并将符合该坐标信息的单元格的属性更改为雷
- 炸弹是在用户第一次点击的时候生成,防止用户第一次点击到炸弹
- 将生成的每个炸弹信息都保存到一个this变量中,方便后续使用
- 遍历每个炸弹周围的非炸弹方格,每遍历一次value值+1
鼠标左键点击
- 点击的时候需要考虑该单元格是否有被标记小旗子(isFlag属性),如果有则无法点击
- 判断是雷还是数字,雷的话则游戏结束,数字则继续判断是否等于0,等于0则使用递归显示空白区域
- 每次打开一个单元格,需要更改该单元格的isOpen属性,表示单元格被打开
鼠标右键点击
- 点击时需要考虑该单元格的isOpen属性是否被打开,打开的话则无法点击
- 当该单元格没有标记旗帜时标记,如果有标记旗帜则取消标记
- 每标记一个方格,剩余炸弹数量-1,取消标记则+1
游戏结束
- 当左键点击到炸弹的时候游戏结束。失败
- 剩余炸弹数量为0时。判断旗帜标记是否正确,正确游戏胜利,标记有误则失败
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
|
<!DOCTYPE html> < html lang = "en" > < head > < meta charset = "UTF-8" > < meta name = "viewport" content = "width=device-width, initial-scale=1.0" > < title >Document</ title > < link rel = "stylesheet" href = "css/index.css" > </ head > < body > < div class = "main" > < header class = "header" > < button >初级</ button > < button >中级</ button > < button >高级</ button > </ header > < div class = "gameBox" id = "gameBox" ></ div > < footer class = "footer" >剩余雷数量:< span id = "surplusMine" ></ span > </ footer > </ div > </ body > < script src = "js/index.js" ></ script > </ html > |
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
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
|
.main .header { text-align : center ; margin : 20px auto ; } .main .gameBox table { border-spacing : 1px ; background-color : rgb ( 170 , 170 , 170 ); text-align : center ; margin : 20px auto ; } .main .gameBox table td.mine { /* 游戏结束时显示 */ border : none ; background : url (./../img/mine.png) no-repeat ; background- size : 90% 90% ; background-color : #e9e6e6 ; background-position : 2px 0 ; } .main .gameBox table td.targetMine { /* 游戏结束时显示,触发雷的单元格 */ border : none ; background : url (./../img/mine.png) no-repeat ; background- size : 90% 90% ; background-color : #ff4b4b ; background-position : 2px 0 ; } .main .gameBox table td.targetFlag { /* 右键标记方格时显示 */ background : url (./../img/flag.png) no-repeat ; background- size : 90% 90% ; background-position : 2px 0 ; background-color : #e9e6e6 ; } .main .gameBox table td { /* 单元格初始样式 */ width : 20px ; height : 20px ; box-sizing: border-box; border : 2px solid ; border-color : #eee #ccc #ccc #eee ; background-color : #e9e6e6 ; font-size : 1px ; font-weight : 800 ; } .gameBox table td.zero, .gameBox table td.one, .gameBox table td.two, .gameBox table td.three, .gameBox table td.four, .gameBox table td.five, .gameBox table td.six, .gameBox table td.seven, .gameBox table td.eight, .gameBox table td.nine { border : none ; background-color : rgb ( 211 , 200 , 200 ); } .gameBox table td.zero {} .gameBox table td.one { color : blue ; } .gameBox table td.two { color : rgb ( 5 , 93 , 5 ); } .gameBox table td.three { color : #008c8c ; } .gameBox table td.four { color : crimson; } .gameBox table td.five { color : rgb ( 228 , 91 , 0 ); } .gameBox table td.six { color : darkorange; } .gameBox table td.seven { color : rgb ( 193 , 196 , 50 ); } .gameBox table td.eight { color : pink; } .main .footer { text-align : center ; } |
javaScript代码
核心代码
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
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
|
function Game(tr, td, mineNum) { this .td = td; this .tr = tr; this .mineNum = mineNum; //存储预设或设定的炸弹总数,用于后续判断是否胜利使用 this .surplusMine = 0; //剩余雷数 this .mineInfo = []; //用于接收随机生成的雷的信息 this .tdsArr = [] //存放单元格的信息 this .isPlay = false ; //是否开始玩 this .openClass = [ "zero" , "one" , "two" , "three" , "four" , "five" , "six" , "seven" , "eight" , "nine" ] this .gameBox = document.getElementById( "gameBox" ); this .table = document.createElement( "table" ); //生成table标签 this .footerNum = document.getElementById( "surplusMine" ); //剩余炸弹数量显示框 } Game.prototype.creatDom = function () { //创建游戏区域,在玩家第一次点击游戏区域的时候执行 this .table.oncontextmenu = function () { return false }; //清除默认右键单机事件 for ( var i = 0; i < this .gameBox.children.length; i++) { //为防止重新开始游戏时,重复生成多个table,在添加之前先删除之前的 var childNod = this .gameBox.children[i]; this .gameBox.removeChild(childNod); } for ( var i = 0; i < this .tr; i++) { var tr = document.createElement( "tr" ); this .tdsArr[i] = []; //为每一行生成一个数组 for ( var j = 0; j < this .td; j++) { var td = document.createElement( "td" ); tr.appendChild(td); //将生成的td插入到tr中 this .tdsArr[i][j] = td; td.info = { //info属性包括了单元格的所有信息,很重要 type: "number" , //格子类型,用于判断是否时炸弹 x: i, //行 y: j, //列 value: 0, //当该格子周围有炸弹时显示该数值,生成炸弹的时候会++ isOpen: false , //判断该单元格是否被打开 isFlag: false //判断是否有标记flag } } this .table.appendChild(tr); //见tr插入到table中 } this .gameBox.appendChild( this .table); } Game.prototype.creatMine = function (event, target) { //生成炸弹,该方法会在用户第一次点击棋盘的时候执行一次 var This = this ; for ( var i = 0; true ; i++) { //随机生成炸弹,生成扎当数与设定扎当书mineNum相同时终止循环 var randomX = Math.floor(Math.random() * this .tr), //随机生成炸弹的行数 randomY = Math.floor(Math.random() * this .td); //随机生成炸弹的列数 // console.log(randomX + " " + randomY) if (target.info.x != randomX || target.info.y != randomY) { //保证第一次点击的时候不是炸弹 if ( this .tdsArr[randomX][randomY].info.type != "mine" ) { //保证每次生成的雷的位置不重复 this .tdsArr[randomX][randomY].info.type = "mine" ; //单元格更改属性为雷 this .surplusMine++; //生成雷的数量+1 this .mineInfo.push( this .tdsArr[randomX][randomY]); //将生成的雷的信息存放到this变量中,方便后续使用 } if ( this .surplusMine >= this .mineNum) { //当生成的炸弹数量等于设定的数量后跳出循环 break ; } } } //为每个炸弹周围的方格添加数字 for ( var i = 0; i < this .mineInfo.length; i++) { var around = this .getAround( this .mineInfo[i], This); //获取每个炸弹的周围方格 // console.log(this.getAround(this.mineInfo[i], This)) for ( var j = 0; j < around.length; j++) { //将周围每个方格的value++ around[j].info.value += 1; } } } Game.prototype.getAround = function (thisCell, This) { //获取某个方格的周围非炸弹方格,需要传递一个单元格dom元素,Game的this var x = thisCell.info.x, //行 y = thisCell.info.y, //列 result = []; // x-1,y-1 x-1,y x-1,y+1 // x,y-1 x,y x,y+1 // x+1,y-1 x+1y x+1,y+1 for ( var j = x - 1; j <= x + 1; j++) { for ( var k = y - 1; k <= y + 1; k++) { if ( //游戏区域的边界,行数x和列数y不能为负数,且不能超过设定的行数和列数 j < 0 || k < 0 || j > (This.tr - 1) || k > (This.td - 1) || //同时跳过自身和周边是雷的方格 This.tdsArr[j][k].info.type == "mine" || (j == x && k == y) ) { continue ; //满足上述条件是则跳过当此循环; } else { result.push(This.tdsArr[j][k]) //将符合的单元格push到result中返回 } } } return result; } Game.prototype.lifeMouse = function (event, target) { //左键点击事件 var This = this ; //用变量的方式将Game的this传递到函数中 var noOpen = 0; //没有被打开的格子数量 if (!target.info.isFlag) { //表示该必须没有被右键标记才能鼠标左击 if (target.info.type == "number" ) { //是数字时,则可视化 function getAllZero(target, This) { //递归函数 // console.log(target.info) if (target.info.isFlag) { //当这个单元格之前有被标记过flag时,则将剩余炸弹数+1 This.surplusMine += 1; target.info.isFlag = false ; //单元格被打开后初始化flag } if (target.info.value == 0) { //等于格子的value等于0的时候 target.className = This.openClass[target.info.value]; //可视化 target.info.isOpen = true ; //表示该单元格被打开 var thisAround = This.getAround(target, This); //获取该单元格周围的格子信息 for ( var i = 0; i < thisAround.length; i++) { // console.log(thisAround[i].info.isOpen) if (!thisAround[i].info.isOpen) { //递归的条件,当格子的open为true时不执行 getAllZero(thisAround[i], This) //执行递归 } } } else { target.innerHTML = target.info.value; target.className = This.openClass[target.info.value]; //可视化 target.info.isOpen = true ; //表示单元格被打开 target.info.isFlag = false ; //单元格被打开后初始化flag } } getAllZero(target, This); //首次执行 //每次鼠标左键点击的时候都需要检查一下没有被打开的方格数量,每有一个则noOpen++ for ( var i = 0; i < this .tr; i++) { for ( var j = 0; j < this .tr; j++) { if ( this .tdsArr[i][j].info.isOpen == false ) { noOpen++; } } } //当noOpen的数量与炸弹数量相同时,说明剩余的方格全是雷,游戏通过 if (noOpen == this .mineNum) { console.log(noOpen) this .gameWin(); } } else { //点击到了炸弹,游戏结束 this .gameOver(target) } } } Game.prototype.rightMouse = function (target) { //鼠标右键点击执行 if (!target.info.isOpen) { if (!target.info.isFlag) { //标记 target.className = "targetFlag" ; //显示旗帜 target.info.isFlag = true ; //表示该方格已经被标记 this .surplusMine -= 1; //每标记一个方格,剩余炸弹数量-=1 // console.log(this.surplusMine) } else { //取消标记 target.className = "" ; //去掉旗帜 target.info.isFlag = false ; this .surplusMine += 1; // console.log(this.surplusMine) } var isWin = true ; if ( this .surplusMine == 0) { //标记完所有flag时,遍历所有单元格 // console.log(this.mineInfo.length) for ( var i = 0; i < this .mineInfo.length; i++) { console.log( this .mineInfo[i].info.isFlag) if (! this .mineInfo[i].info.isFlag) { //检查每个雷的isFlag属性是否被标记,只要有一个为false则输掉游戏 isWin = false ; this .gameOver(target, 1); break ; } } isWin ? this .gameWin(1) : 0; //三目运算符号 } // if (this.surplusMine == 0) { //标记完所有flag时,遍历所有单元格 // for (var i; i < this.tr; i++) { // for (var j; j < this.td; j++) { // if() // } // } // } } } Game.prototype.gameOver = function (target, code) { //游戏结束,code为触发代码,当旗用完了时为1,点击到炸弹为0 // console.log(this.mineInfo) var mineInfoLen = this .mineInfo.length; for ( var i = 0; i < mineInfoLen; i++) { //显示每个雷的位置 this .mineInfo[i].className = "mine" ; } this .table.onmousedown = false ; //取消鼠标事件 if (code) { alert( "旗帜用完了,没有排除所有雷,游戏结束" ) } else { target.className = "targetMine" ; //触发雷标红色 alert( "你被炸弹炸死了,游戏结束" ) } } Game.prototype.gameWin = function (code) { //游戏胜利 if (code) { alert( "你成功标记所有地雷,游戏通过" ) } else { alert( "你找到了所有安全区域,游戏通过" ) } this .table.onmousedown = false ; } Game.prototype.play = function () { var This = this ; //需要将this传递到事件函数中使用 this .table.onmousedown = function (event) { event = event || window.event; //兼容IE target = event.target || event.srcElement //兼容IE if (! this .isPlay) { //首次点击初始化棋盘,随机生成炸弹 this .isPlay = true ; This.creatMine(event, target); } if (event.button == 0) { //鼠标左键点击时执行 This.lifeMouse(event, target); } else if (event.button == 2) { //右键点击执行 This.rightMouse(target) } This.footerNum.innerHTML = This.surplusMine; //每次点击右键,刷新页面下方的剩余雷数 } } Game.prototype.tablePos = function () { //将table居中显示 var width = this .table.offsetWidth, height = this .table.offsetHeight; // console.log(this.table.offsetWidth) this .table.style.width = width + "px " ; this .table.style.height = height + "px " } function addEvent(elem, type, handle) { //添加事件函数 if (elem.addEventListener) { //w3c标准 elem.addEventListener(type, handle, false ); } else if (elem.attachEvent) { //IE9及以下 elem.attachEvent( "on" + type, function () { handle.call(elem); }) } else { //其他情况 elem[ "on" + type] = handle; } } Game.prototype.setDegree = function () { //调整难度 var button = document.getElementsByTagName( "button" ); addEvent(button[0], "click" , function () { //简单 var game = new Game(10, 10, 10); game.creatDom(); game.play(); game.tablePos(); }); addEvent(button[1], "click" , function () { //一般 var game = new Game(16, 16, 50); game.creatDom(); game.play(); game.tablePos(); }); addEvent(button[2], "click" , function () { //困难 var game = new Game(30, 30, 125); game.creatDom(); game.play(); game.tablePos(); }); } // 默认棋盘 var game = new Game(10, 10, 10); game.creatDom(); game.play(); game.tablePos(); game.setDegree() |
总结一下,该游戏个人觉得难点有4个:
- 没有思路,在bilibili看了一个教学视频,但是比较难理解,在原有基础上增加了自己的一些想法
- 递归
- 获取某一个方格周围的八个方格
- 多层的if嵌套和循环
缺点:
- 性能不佳,存在大量for循环,且没有优化
- 某些时候界面显示的剩余炸弹数量不准确(已修复)
- 代码冗余较多
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持服务器之家。
原文链接:https://blog.csdn.net/weixin_47582190/article/details/110186059