如何使用原生JS,快速写出一个扫雷小游戏

语言: CN / TW / HK

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第2天,点击查看活动详情

1.绘制游戏区域

16*16的二维数组,双层遍历之后,第一层创建ul标签,第二层创建button标签。为什么用button标签,因为button标签自带点击效果,不需要再额外设置。渲染完之后加上CSS样式,游戏区域就写好了。利用数组来渲染游戏区域,识别查找还有修改起来会很方便。

let buttonArr = [ [{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}], [{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}], [{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}], [{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}], [{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}], [{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}], [{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}], [{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}], [{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}], [{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}], [{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}], [{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}], [{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}], [{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}], [{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}], [{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}], ] //渲染游戏区域函数 const renderDiv = () => { document.querySelector('div').innerHTML = '' buttonArr.forEach((item, index) => { let ul = document.createElement('ul') //给ul标签添加自定义属性y ul.dataset.y = index item.forEach((item2, index2) => { let button = document.createElement('button')//遍历数组,绘制棋盘 //给button标签添加自定义属性x,用来作为坐标使用 button.dataset.x = index2 if (item2.num === 10) { //给地雷元素添加一个自定义属性,便于识别 button.dataset.z = 10 //写的时候可以把地雷先渲染出来,写完了再注释掉 // button.classList.add('active') } else { item2.num = 0 } ul.appendChild(button) }) document.querySelector('div').appendChild(ul) }) } renderDiv() CSS

* { margin: 0; padding: 0; box-sizing: border-box; list-style: none; } div { width: 500px; margin: 50px auto; padding: 5px; border: 5px solid black; } ul { display: flex; height: 30px; } button { width: 30px; height: 30px; background-color: #c0c0c0; } .bgc1 { background-color: white; } .bgc2 { background-color: black; } .bgc3 { background: url(./pngsucai_1307487_8c9867.png)no-repeat; background-color: #c0c0c0; background-size: 100%; } .active { background: url(./Snipaste_2022-06-12_16-24-48.jpg) no-repeat; background-size: 100%; } p { position: absolute; top: 200px; right: 200px; font-size: 20px; }

```

鼠标左键点击
鼠标右键标记
点击空白格子可快速扫雷

```

02.jpg

2.生成地雷

一共40个地雷,通过Math方法获取随机数X和Y,把这两个数作为坐标存入数组中,数组长度为40时就去重,然后接着获取坐标,直到40个坐标没有重复,就跳出循环。地雷的数量也可以随意调整,要记得给有地雷的格子添加一种自定义属性,这样数组里面元素对应的页面标签就可以很方便的联系起来。

//生成地雷 const lei = () => { let leiArr = [] function fn() { //获取随机坐标 const x = Math.floor(Math.random() * 16) const y = Math.floor(Math.random() * 16) leiArr.push([y, x]) if (leiArr.length == 40) { //数组去重 let obj = {} leiArr.forEach(item => obj[item] = item) leiArr = Object.values(obj) } if (leiArr.length == 40) { return } fn() } fn() return leiArr } //渲染地雷 const renderLei = (arr) => { //把地雷对应的对象里面添加一个数据,便于识别 arr.forEach(item => { buttonArr[item[0]][item[1]].num = 10 }) } renderLei(lei()) renderDiv()

03.jpg

3.给每一个格子添加数字,周围有几个地雷就是几,没有地雷就是空

把所有不是地雷的格子都遍历一遍,然后把每个格子周围一圈的格子都获取到,接着判断这一圈格子里面有几个地雷,当前格子的数字就是几。

获取周围一圈格子有点点复杂,获取格子的逻辑就是当前格子上下1行内,X坐标相差为1或者0的格子。中间区域的格子都是从上中下3行内的格子进行获取。第一行和最后一行的格子就只用获取两行。

因为每一个格子的默认num值都是0,格子内数字可以通过遍历周围一圈的格子,然后有地雷就给num值+1,利用遍历累加的方法,这样num值就是对应的地雷的数量了。渲染的时候就把大于0的num值显示出来就可以了。

``` //渲染格子数字 const renderNum = () => { buttonArr.forEach((item, i) => { item.forEach((item02, i02) => { if (item02.num == 0) { //获取不是地雷的标签 let ul = document.querySelectorAll('ul')[i] let btn = ul.querySelectorAll('button')[i02] //调用获取格子数字的函数,传入3个参数,当前格子对应的数组元素,当前行,和当前格子 getNum(item02, ul, btn) //判断这个格子是否带有数字 if (item02.num < 10 && item02.num > 0) { //给有数字的格子添加一个自定义属性,便于识别 btn.dataset.z = 1 const span = document.createElement('span') //将数字渲染到格子中 span.innerText = item02.num span.style.display = 'none' btn.appendChild(span) } } }) })

} //获取格子数字 const allUl = document.querySelectorAll('ul') let getNumArr = [] ////获取格子数字函数 const getNum = (item02, ul, btn) => { //建立一个存放目标格子周围一圈格子的数组 getNumArr = [] const y = ul.dataset.y const x = btn.dataset.x //调用获取格子周围一圈格子的函数,将格子的坐标传入参数 getBox(x, y) //遍历这个格子周围一圈的格子,有地雷的话num就+1 getNumArr.forEach(item1 => { if (item1.dataset.z == 10) { item02.num++ } }) } //获取格子周围一圈格子的函数 function getBox(x, y) { //第一排的格子只需要选中前两排 if (y == 0) { for (let i = 0; i < 2; i++) { //选中前两排的格子 const allButton01 = allUl[i].querySelectorAll('button') //如果两个格子x坐标相减的绝对值小于或等于1,那么这两个格子就是相邻的 let getNumArr02 = Array.from(allButton01).filter(item => Math.abs(item.dataset.x - x) <= 1) //加入数组 getNumArr.push(...getNumArr02) } } //第二排至倒数第二排,需要选中自身上中下三排的格子 else if (y >= 1 && y < 15) { for (let i = +y - 1; i < +y + 2; i++) { const allButton02 = allUl[i].querySelectorAll('button') let getNumArr02 = Array.from(allButton02).filter(item => Math.abs(item.dataset.x - x) <= 1) getNumArr.push(...getNumArr02) } } else { //最后一排,选中两排的格子遍历 for (let i = 14; i < 16; i++) { const allButton03 = allUl[i].querySelectorAll('button') let getNumArr02 = Array.from(allButton03).filter(item => Math.abs(item.dataset.x - x) <= 1) getNumArr.push(...getNumArr02) } } } renderNum() ```

微信图片_20220613103206.png

4.鼠标点击事件

数字渲染出来之后,接下来就是点击事件了。点击事件分两个,鼠标左键点击和鼠标右键插旗。

首先把地雷和数字的样式都隐藏,鼠标点击实际上就是一个添加样式的过程。

左键点击的时候,要先判断点击的是否是地雷。是地雷的话就游戏结束。不是地雷就给它添加一个CSS样式,并且让数字显示出来。

如果点击的是空白格子,就需要把这个空白格子所连接的所有非地雷格子都显示出来,就是那种点击一个显示一大片的效果。

点击的如果是数字,就是显示这一个格子。

鼠标右键就很简单了,直接添加CSS样式,起到一个插旗子的效果。但是要判断一下,以经点过的格子就不能插旗子了,只能给没点过的格子添加红旗。

``` //点击事件 let allArr = [] //声明一个用来判断获胜的数组 allUl.forEach(buttons => { buttons.querySelectorAll('button').forEach(item => { item.addEventListener('click', function () { //点击效果 this.classList.add('bgc1') //如果这个格子有数字,就显示 if (this.querySelector('span')) { this.querySelector('span').style.display = 'block' } allArr.push(this) //判断,如果点击的是地雷,游戏结束 if (item.dataset.z == 10) { item.classList.add('active') setTimeout(function () { alert('游戏失败') location.reload() }, 200) } //只有空白的格子没有自定义的z属性,判断如果点击的是空白格子 if (!item.dataset.z) { const num0Arr = [] num0(item) //空白格子的周围一圈一定没有地雷,直接让这些格子显示类容 function num0(item) { getNumArr = [] const buttons = item.parentNode const y = buttons.dataset.y const x = item.dataset.x //再次调用获取周围一圈格子的函数 getBox(x, y) getNumArr.forEach(itemBtn => { //点击空白格,就会自动把周围一圈的格子都显示 if (itemBtn.dataset.z != 10) { itemBtn.classList.add('bgc1') } if (itemBtn.querySelector('span')) { itemBtn.querySelector('span').style.display = 'block' } //将这些格子都加入总数组中 allArr.push(itemBtn) if (!itemBtn.dataset.z) { //如果空白格周围一圈格子里面还有空白格,就将他们加入这个num0数组中,稍后再次循环一次这个点击事件 num0Arr.push(itemBtn) } }) } //给空白格周围的空白格也添加一个显示类容的函数 function clickNum0() { //num0Arr包含了点击的空白格周围9个格子内的所有空白格子,newNum0Arr就是除掉自身的所有空白格子 const newNum0Arr = num0Arr.filter(item2 => item2 != item) newNum0Arr.forEach(item02 => { //再次在其他空白格身上调用显示周围一圈内容的函数,这样就形成点击一个空白格子, //如果这个格子周围空白区域很多,能显示一大片区域的效果。 num0(item02) }) } clickNum0() } //给总数组去重 const newAllArr = [...new Set(allArr)] //筛选,排除是地雷的格子 const newAllArr02 = newAllArr.filter(item => item.dataset.z != 10) //一共256个格子,40个地雷,如果数组长度达到216,就可以判定胜利了。 if (newAllArr02.length == 216) { alert('游戏胜利!') location.reload() } }) //鼠标右键点击事件,用来插棋子 item.addEventListener('contextmenu', function () { if (!this.classList.contains('bgc3') && !this.classList.contains('bgc1')) { this.classList.add('bgc3') } else { this.classList.remove('bgc3') }

})

}) })

```

01.jpg 空白格子的点击事件是这个游戏麻烦的地方。但是只要清楚一点,空白格子周围一圈是一定没有地雷的。点击空白的格子,就相当于把周围的9个格子全部都点了一遍。在这个逻辑上再去写代码,就不会很难了。

- 浏览器里面点鼠标右键,页面会弹出来菜单,我们需要把这个屏蔽掉。

//鼠标右键屏蔽菜单 document.oncontextmenu = function (event) { if (window.event) { event = window.event; } try { var the = event.srcElement; if (!((the.tagName == "INPUT" && the.type.toLowerCase() == "text") || the.tagName == "TEXTAREA")) { return false; } return true; } catch (e) { return false; } }

写到这里,整个游戏就写完了,一共也就200多行代码。

06.jpg

05.jpg