likes
comments
collection
share

javascript 写一个简易扫雷游戏(不用canvas)

作者站长头像
站长
· 阅读数 66

游戏逻辑:

  1. 首先随机在格子内生成若干个雷;
  2. 当玩家点击一个格子时: (1)如果是雷,那么展示出所有的雷,游戏结束; (2)如果不是雷:
  • 如果周围9宫格内有雷,则显示出雷的个数;
  • 如果自己周围没有雷,则不显示数字

先生成基本的样子

样子如下,目前点击后格子会变成黄色  javascript 写一个简易扫雷游戏(不用canvas)

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,也很好算。

所以再理一下:

  1. 用0-99存放雷的格子
  2. 新建一个数组用来存放value,长度为100,默认值都为0;
  3. 遍历雷数组,将雷自己的格子设置为-1,将每个雷周围的格子value++
  4. 遍历grid元素,如果元素value为-1,插入一个图片代表雷,如果value为0,什么也不做,如果value为其他,放入数字。并为每个雷元素放入对应class(例如hasMine),所有内部元素加上hide
  5. 点击元素时,内部元素hide取消。通过对应class来判断点中的是否是雷,是雷的话将所有hide取消。

写的过程中发现上面有一些问题,计算周围九宫格格子要负责一些,当格子在边角时的情况也需要考虑。

写到下面这个程度时,(显不隐藏格子内容),已经能正常显示出我们想要的内容。只不过点击内容还没写。

 javascript 写一个简易扫雷游戏(不用canvas)

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 class="hidden">${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
评论
请登录