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

持续创作,加速成长!这是我参与「掘金日新计划 · 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;
    }
复制代码
<body>
  <div></div>
  <p>鼠标左键点击<br>
    鼠标右键标记<br>
    点击空白格子可快速扫雷
  </p>
  <script src="./扫雷.js"></script>
</body>
复制代码

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

猜你喜欢

转载自juejin.im/post/7108316286593007646