javascript 写一个简易扫雷游戏(不用canvas)
游戏逻辑:
首先随机在格子内生成若干个雷;
当玩家点击一个格子时: (1)如果是雷,那么展示出所有的雷,游戏结束; (2)如果不是雷:
如果周围9宫格内有雷,则显示出雷的个数;
如果自己周围没有雷,则不显示数字
先生成基本的样子
样子如下,目前点击后格子会变成黄色
class MineSweep { constructor() { // 这样也可以让用户赋值,但会更复杂一些 this.gridNumPerRow = 10 this.mineNum = 10 } init() { this.generateMines() this.generateBoard() console.log(this.generateMines()) } /** * 生成扫雷基本的格子,直接用div,借助css grid */ generateBoard() { const $wrapper = document.createElement('div') $wrapper.classList.add('board-wrapper') document.body.appendChild($wrapper) let $grid; for (let i = 0; i < this.gridNumPerRow * this.gridNumPerRow; i++) { $grid = document.createElement('div') $grid.classList.add('grid') $wrapper.appendChild($grid) $grid.onclick = this.handleClickGrid } } /** * 格子被点击后的处理 */ handleClickGrid() { e.target.style.backgroundColor = 'yellow' } /** * 随机生成雷的位置 * 要点是要注意不能有重复位置的雷 */ generateMines() { // 这个函数重点是考虑如何生成不重复的雷,我刚开始想的是用把[row, column] 数组存入结果数组, // 但如果这样的话,比对是否和前面有重复不方便,所以后来改成行用一个数组,列用一个数组,这样我们可以用includes直接判断是否数字有重复 const rows = [] const columns = [] let generatedRow let generatedColumn for (let i = 0; i < 10; i++) { // 如果有重复,则继续生成 do { generatedRow = parseInt(Math.random() * 10) generatedColumn = parseInt(Math.random() * 10) } while (rows.includes(generatedRow) && columns.includes(generatedColumn)) // 已拿到未重复的数字对,放入数组中 rows.push(generatedRow) columns.push(generatedColumn) } return { rows, columns } } } 复制代码
css
.board-wrapper { width: 400px; display: grid; background-color: green; gap: 0; grid-template: repeat(10, 1fr) / repeat(10, 1fr); } .grid { background-color: rgb(233, 233, 233); width: 40px; height: 40px; border: 1px solid rgb(96, 96, 96); } 复制代码
现在已经有了基本的样子,考虑点击后显示数字/雷/空白
实现点击格子后出现内容
思路1:让数字、雷元素一开始就已经加入再对应格子中,只不过其对应的div有个class是hide,所以它不可见。点击后这些内容可见,点到雷后,所有格子hide取消。
思路2: 给每个格子添加属性value, -1表示自己是雷,其他数字表示周围雷的个数。只要这个属性添加好后,直接遍历,给每个格子添加对应innerHTML即可;
思路3: 如何计算格子周围的雷的数量?假设要计算(3,3)格子周围的雷的数量,那么需要check周围8个格子,要计算(3,4)格子周围雷数量的话,周围格子有的和(3,3)是重叠的,又要check一次 。从另一个角度想,雷的数量比较少,我们可以直接遍历有雷的格子,把它周围的格子num++就可以了。
思路4: 用一个含有100个数字的数组来存储每个格子的value。(本来想过用二维数组,后来觉得不需要,因为用0-99就可以区分这100个格子,而不是一定要用坐标来区分。不过这样的问题是:我们计算雷的个数时要用到坐标,坐标和0-99怎么转化?)
思路5: 考虑把前面生成雷的函数由行列数组改为数字,例如生成88代表第88个格子是雷。然后88对应的九宫格其他格子为:78 87 89 98,也很好算。
所以再理一下:
用0-99存放雷的格子
新建一个数组用来存放value,长度为100,默认值都为0;
遍历雷数组,将雷自己的格子设置为-1,将每个雷周围的格子value++
遍历grid元素,如果元素value为-1,插入一个图片代表雷,如果value为0,什么也不做,如果value为其他,放入数字。并为每个雷元素放入对应class(例如hasMine),所有内部元素加上hide
点击元素时,内部元素hide取消。通过对应class来判断点中的是否是雷,是雷的话将所有hide取消。
写的过程中发现上面有一些问题,计算周围九宫格格子要负责一些,当格子在边角时的情况也需要考虑。
写到下面这个程度时,(显不隐藏格子内容),已经能正常显示出我们想要的内容。只不过点击内容还没写。
class MineSweep { constructor() { this.gridNumPerRow = 10 this.mineNum = 20 this.gridValues = [] this.gridHasMine = [] } init() { this.generateMines() this.setGridValue() this.generateBoard() } /** * 生成扫雷基本的格子,直接用div,借助css grid */ generateBoard() { console.log(this.gridHasMine) const $wrapper = document.createElement('div') $wrapper.classList.add('board-wrapper') document.body.appendChild($wrapper) let $grid; for (let i = 0; i < this.gridNumPerRow * this.gridNumPerRow; i++) { $grid = document.createElement('div') $grid.classList.add('grid') $wrapper.appendChild($grid) if (this.gridValues[i] == -1) { // 插入雷元素 $grid.innerHTML = `<img src="./mine.png" class="hidden hasMine mine-img"></span>` } else if (this.gridValues[i] > 0) { $grid.innerHTML = `<span>${this.gridValues[i]}</span>` } $grid.onclick = this.handleClickGrid } } /** * 格子被点击后的处理 */ handleClickGrid(e) { // } /** * 随机生成雷的位置 * 要点是要注意不能有重复位置的雷 */ generateMines() { const res = [] // 用0-99代表每一个格子 let mineGrid for (let i = 0; i < this.mineNum; i++) { // 如果有重复,则继续生成 do { mineGrid = parseInt(Math.random() * 100) } while (res.includes(mineGrid)) // 放入结果数组中 res.push(mineGrid) } this.gridHasMine = res } /** * 为每个格子设置对应的值,-1代表有雷,其他数字代表周围九宫格内雷的个数 */ setGridValue() { // 设置100个默认值 const gridCount = this.gridNumPerRow * this.gridNumPerRow for (let i = 0; i < gridCount; i++) { this.gridValues.push(0) } // 用一个数组存放周围的8个格子的index,如果不存在设置为null let arroundGrids for (let i = 0; i < this.gridHasMine.length; i++) { // 获取当前雷对应的格子 const currGrid = this.gridHasMine[i] // 先将雷对应的格子设置为-1 this.gridValues[currGrid] = -1 arroundGrids = this.getAroundGrids(currGrid) console.log(currGrid) console.log(arroundGrids) //将周围存在的格子value加1 // 需要注意如果周围格子已经有雷,则不需要进行加1操作 for (let i = 0; i < arroundGrids.length; i++) { if (arroundGrids[i] && this.gridValues[arroundGrids[i]] != -1) { this.gridValues[arroundGrids[i]]++ } } } } /** * 获取周围九宫格内格子对应的index */ getAroundGrids(currGrid) { let one = this.isValidGridNumber(currGrid - 11) ? currGrid - 11 : null let two = this.isValidGridNumber(currGrid - 10) ? currGrid - 10 : null let three = this.isValidGridNumber(currGrid - 9) ? currGrid - 9 : null let four = this.isValidGridNumber(currGrid - 1) ? currGrid - 1 : null let five = this.isValidGridNumber(currGrid + 1) ? currGrid + 1 : null let six = this.isValidGridNumber(currGrid + 9) ? currGrid + 9 : null let seven = this.isValidGridNumber(currGrid + 10) ? currGrid + 10 : null let eight = this.isValidGridNumber(currGrid + 11) ? currGrid + 11 : null // 没有左边格子的话1、4、6对应的都去掉 if (this.notHasLeftNeighbour(currGrid)) { one = null four = null six = null } // 没有右边格子的话3、5、8对应的都去掉 if (this.notHasRightNeighbour(currGrid)) { three = null five = null eight = null } return [one, two, three, four, five, six, seven, eight] } /** * 判断是否左边没有格子 */ notHasLeftNeighbour(index) { return index % 10 == 0 } /** * 判断是否右边没有格子 */ notHasRightNeighbour(index) { return index % 10 == 9 } /** * 判断是否是存在的格子 */ isValidGridNumber(num) { return num >= 0 && num <= 99 } } 复制代码
接下来只要实现点击事件就ok啦。最终完成的样子如下。当然还可以再做一些优化,例如点到雷后提示用户本轮游戏失败。或者再加一个重新开始按钮。不过我这个练习暂且先做到这里。 点击时间再这里,完整的代码和示范在最后
/** * 格子被点击后的处理 */ handleClickGrid(e) { let $hiddenContent // 避免只将图片部分或文字背景改变,而整个方框背景未变 if (e.target.tagName == 'IMG' || e.target.tagName == 'SPAN') { $hiddenContent = e.target e.target.parentNode.style.backgroundColor = 'yellow' } else { e.target.style.backgroundColor = 'yellow' $hiddenContent = e.target.firstChild } // 为空说明value为0 if (!$hiddenContent) { return } // 点到雷后显示所有隐藏内容 if ($hiddenContent.classList.contains('hasMine')) { const $hiddens = document.getElementsByClassName('hidden') while ($hiddens.length) { $hiddens[0].classList.remove('hidden') } // 下面这段代码会有问题! // for (let i = 0; i < $hiddens.length; i++) { // $hiddens[i].classList.remove('hidden') // } } else { // 加上判断,避免多次点击同一个格子报错 if ($hiddenContent.classList.contains('hidden')) { $hiddenContent.classList.remove('hidden') } } } 复制代码
这里需要注意的是下面这段代码
const $hiddens = document.getElementsByClassName('hidden') while ($hiddens.length) { $hiddens[0].classList.remove('hidden') } // 下面这段代码会有问题! // for (let i = 0; i < $hiddens.length; i++) { // $hiddens[i].classList.remove('hidden') // } 复制代码
用class获取到的元素数组是动态的,所以随着remove,元素的数量是越来越少的,会导致结果不正确。正确做法是用while来判断数组内是否还有内容。
作者:两个月牙里的星星
链接:https://juejin.cn/post/7169981611016978468