29

阿望教你用vue写扫雷(超详细哦)

 4 years ago
source link: https://awang0608.github.io/2020/01/19/阿望教你用vue写扫雷-超详细哦/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

前言

话说阿望还在大学时,某一天寝室突然停网了,于是和室友两人不约而同地打开了扫雷,比相同难度下谁更快找出全部的雷,玩得不亦乐乎,就这样,扫雷伴我们度过了断网的一周,是整整一周啊,不用上课的那种,可想而知我们是有多无聊了。

这两天临近过年了,该放假的已经放假了,不该放假的已经请假了,公交不打挤了,地铁口不堵了,公司也去了大半部分的人了,就留阿望这种不得不留下来值班的人守着空荡荡的办公室了,于是,多年前那种无所事事的断网心态再次袭来,于是,想玩扫雷的心再次蹦跶出来,于是,点开了电脑的附件,于是,发现我的电脑上并没有扫雷【手动微笑.jpg】。

怎么想起要写扫雷的阿望就不多废话了,直接开干。

扫雷游戏规则

想当年阿望还是在大学才参透扫雷的游戏规则的,初高中的时候都是靠运气瞎点,从没赢过,当然了,境界提升以后,追求的自然就不是赢了,而是速度。

来,看规则:

jQRBJrY.jpg!web

大白话游戏规则,计算逻辑如下:

  1. 游戏开始,选择难度
  2. 鼠标左键随机点开一个格子A(你想用右键标雷也可以)
  3. 左键点击后一共有三种情况 1) 是数字,假设是2,表示以该格子A为中心的九宫格除了该格子以外的8个格子内有2个雷 2)是空白,表示以该格子A为中心的九宫格除了该格子以外的8个格子内没有雷,并且系统计算分别以这8个格子为中心九宫格除了该格子以外的8个格子是否是空白 a. 是空白,继续计算以这8个格子为中心九宫格除了该格子以外的8个格子是否是空白,循环扩散 b. 是格子边界,停止边界扩散 c. 是数字,数字格子翻出来显示,停止扩散 3)是雷,game over 了。。。
  4. 如果点第一次只翻了一个数字是比较难开始游戏的,如果第一次翻了一大片出来,恭喜你,可以开始秀你的智商了,在确认是雷点的格子点右键,即可标记地雷格,表示你认定这个格子是地雷
  5. 如果数字周围的雷被全部标记出来了,但数字周围还有没被翻开的格子的话,双击该格子,自动将其他格子翻开

好,这下规则我们了解了,接下来,摩拳擦掌,开始写代码咯,本次代码用vue来写,没有原因,习惯了而已。

扫雷代码逻辑

看着游戏规则,我们先来理一理,要如何完成这个功能,我们以最简单的,最能使人理解的步骤,来完成这次功能。

  1. 难度选择
  2. 根据选择的游戏难度绘制游戏主界面,就是格盘
  3. 根据选择的游戏难度随机生成地雷放到盘中
  4. 计算每个格子周围的雷数
  5. 完成用户交互(左键点击翻格子,右键点击放雷)
  6. 空白格扩散翻格子
  7. 计算游戏结束

生成项目

避免vue新手不知如何下手,那就从建项目开始吧,环境安装我就不讲了,脚手架也是要有的

第一步,初始化lovesweeping工程(阿望不喜欢地雷,喜欢小桃心)

项目很小,不需要路由,不需要vuex,即可完成,只带了sass,连eslint都可以不要,看官们可以根据自己的喜好建项目

6bQjYjY.png!web

项目生成之后,helloworld就可以删掉了,它的存在并没有什么意义

文件切分

这一步主要是构建工程结构,简单画一下主要的几个文件

- src
    - components
        - SelectLevel.vue [新增,难度选择组件]
        - LoveSweeping.vue [新增,游戏界面组件]
    - App.vue [父组件,负责组件间的切换和某些数据传递]
    - main.js
- package.json

好,初步认识了项目结构以后,咱把该新建的建好,可以不加东西,不报错就行

然后就是把组件间的切换代码写好,再来一步步的填充代码

ua6NvyE.jpg!web

难度选择

这一步很简单,首先画好难度选择的页面,难度可以自己设置,你觉得合理就行,我这里的数据格式是这样的,用一个对象表示一个难度等级,对象中包含了难度描述,以及难度设置,设置中包含了格子横排数,格子纵排数,雷数

// 难度
    level: [
        {
            text: '青铜', // 难度描述
            value: [9, 9, 10] // 格子横排数,格子纵排数,雷数
        }, {
            text: '黄金',
            value: [12, 9, 20] 
    ]

然后模板中直接渲染列表,这样做的好处是,想要增加难度直接在数组中添加数据即可

<li v-for="(item, index) in level" :key="index" @click="handleChoseLevel(item.value)"></li>

该组件中只有一个方法:选择难度之后,跳转到游戏主界面上去,因为项目没有用路由,直接使用组件间的切换,所以,这个方法只负责告诉父组件,我已经选择好难度了,可以开始游戏了

// 选择难度
    handleChoseLevel(level) {
        this.$emit("chose-level", level);
    }

代码如下:

FR3u2av.jpg!web

界面长这样,当然,你要觉得难看自己换个样式也行

RvI3qiQ.jpg!web

游戏界面

画格盘

通过游戏难度选择来决定游戏格盘的大小,组件间已通过App将游戏难度传至界面组件中,我们用props把数据接收到,消化成自己的数据,画格盘需要的数据有:横排格子数,纵排格子数

画格子:我们将格子的索引暴露出来,后续可以帮助我们试错。整个格局有两种方式来表示格盘,坐标式和索引式,比如横9纵9的格子,[0, 0]代表第1个格子,[2, 3]代表第三行第四列也就是第20个格子。此次使用索引式来标志格盘

<div 
        v-for="col in cols" :key="Math.random() + col"
        class="game-content-row">
        <span 
            v-for="row in rows" :key="Math.random() + row"
            class="game-block">
            <span></span>
        </span>
    </div>

yQRFRbn.jpg!web

随机生成地雷

首先data中添加一个minePosition属性,用来记录雷点位置

随机生成地雷比较简单,主要注意,生成的地雷点数在格盘个数范围内,那么就可以写出随机生成的地雷了。界面组件已收集到横排格子数、纵排格子数、雷数,那么就能得到格子总数,假设横9纵9,10个雷,那么就是生成10个81以内的随机数(如果索引从0开始,即80以内)。

// 随机获取雷点位置
    getMinePosition() {
        // 定义一个数组装不重复的格点
        let mineArr = [];
        // 循环雷数生成不重复的雷点
        for (let n = 0; n < this.gameInfo[2]; n++) {
            const random = Math.floor(Math.random() * this.latticeNum);
            if (mineArr.indexOf(random) === -1) {
                mineArr.push(random);
            } else {
                n--;
            }
        }
        this.minePosition = mineArr;
    },

把地雷位置暴露出来

ZjQrquQ.jpg!web

格子周围的雷数

确认了雷点位置,接下来要做的就是确认每一个非雷点位置周围的雷的数量,我们用对象来描述一个格子,这个对象主要包含以下几个属性

// 格子属性
    lattice: [{
        index: 0, // 格子索引
        mineNum: 0, // 周围雷数
        isMine: false, // 是否是雷
        isOpen: false, // 是否已经被点开
        isMark: false, // 是否被标记
    }],

这里我们主要用到index, isMine, mineNum属性,这一步,主要是计算每个格子元素的mineNum值,依赖于以下两个方法,个人觉得扫雷游戏最难理解的,最难捋清的逻辑,其中一个就是获取非雷点位置周围8个位置索引的方法getLatticeIndex(另一个是点击空白格扩散)

// 获取格子周围的雷数,
    getMineNumAroundLattice(lattice, index) {
        // 先获取格子周围的有效索引
        const latticeIndexArr = this.getLatticeIndex(index);
        // 循环索引,索引值在雷点数组中的,即为雷,当前格子的雷点数加1
        latticeIndexArr.forEach(i => {
            if (this.minePosition.indexOf(i) > -1) {
                lattice.mineNum ++;
            }
        });
    },
    // 获取格子周围的有效索引
    getLatticeIndex(index) {
        // 存索引值的变量
        let latticeIndexArr = [];
        // 当前格子位于第几行
        const latticeRow = Math.ceil(index / this.rows);
        // 当前格子位于第几列(求余为0说明是最右边一列)
        const latticeCol = Math.ceil(index % this.rows) || this.rows;
        // 第一行没有上一行,不需要计算减1的行值,最后一行没有下一行,不需要计算加1的行值
        for (let i = (latticeRow === 1 ? 0 : -1); i < (latticeRow === this.cols ? 1 : 2); i++) {
            // 第一列没有左列,不需要计算减1的列值,最后一列没有右列,不需要计算加1的列值
            for (let j = (latticeCol === 1 ? 0 : -1); j < (latticeCol === this.rows ? 1 : 2); j++) {
                // 索引值 = (当前行值 + (上一行【-1】/当前行【0】/下一行【+1】) - 1【1是索引从0开始,所以需要减去】) * 每行格子数 + 当前列值 + (上一列【-1】/当前列【0】/下一列【+1】)
                const latticeIndex = (latticeRow + i - 1) * this.rows + (latticeCol + j);
                latticeIndexArr.push(latticeIndex);
            }
        }
        return latticeIndexArr;
    },

有了这两个方法,咱成功地获取到了每个非雷点格子周围的雷的数量,来,展示出来,这样展示的好处是,我们一眼就可以看出算法是否正确

YRFBjmE.jpg!web

没问题了,来,接着往下走,格盘数据基本都设置好了,那我们接下来要做的就是,点开格子操作

点击交互

这一步先做简单点,有个明显的区别就可以了,点雷我们先不管,先看点数字和空白的情况,首先得明确,到时候格子的可见属性是全部要被隐藏的,点击了才会显示出来,这就用到了我们上一步提到的isOpen属性,默认肯定全是不可见的,点击之后,非雷翻开

点数字

点数字很简单,直接翻开,将isOpen属性设置为true

来点不一样的,isOpen === true 的时候字体变红色,走你┏ (゜ω゜)=☞

@click.left="handleClickLattice(lattice[(col - 1) * rows + row - 1])"
// 点了格子
    handleClickLattice(lattice) {
        // 是数字
        if (lattice.mineNum) {
            if (!lattice.isOpen && !lattice.isMark) {
                lattice.isOpen = true;
            }
        } 
    },

点空白

第二个难点来咯,点空白格需要注意以下几点:1、空白格表示周围8格都没有雷 2、扩散周围8格,判断雷数,循环往复 3、遇边界停止扩散,遇数字停止扩散

假设横9纵9的格盘,第二排第三列格为空白格即第12格,那么点了该空白格之后,首先将其与周围8格(2,3,4,11,12,13,20,21,22)一起,isOpen置为true,然后分别以周围8格为中心,判断该格子是数字,停止扩散,是空白格,继续扩散

// 代码把下半部分补齐
    handleClickLattice(lattice) {
        ... else {
                // 是空白
                const latticeIndexArr = this.getLatticeIndex(lattice.index);
                this.showWhiteAround(lattice, latticeIndexArr);
            }
    },
    // 展示周围的空白标记,直至边缘(格子边缘或者数字)
    showWhiteAround(lattice, latticeIndexArr) {
        // 避免有重复的数据停不下来,去个重
        latticeIndexArr = [...new Set(latticeIndexArr)];
        for (let i = 0; i < latticeIndexArr.length; i++) {
            const item = latticeIndexArr[i];
            latticeIndexArr.splice(i, 1);
            i--;
            if (this.lattice[item].isOpen) {
                continue;
            }
            this.lattice[item].isOpen = true;
            if (!this.lattice[item].mineNum) {
                const arr = this.getLatticeIndex(this.lattice[item].index);
                this.showWhiteAround(this.lattice[item], latticeIndexArr.concat(arr));
            } 
        }
    },

这一步写完,基本明面上的扫雷步骤就已经完成了,handleClickLattice方法再加一步判断,如果是雷,游戏结束

Yn2muiJ.jpg!web

右键标记雷点

这个就很简单了,写个右击事件,修改一下格子的isMark和isOpen属性,这一步的基本逻辑就是

  1. 右击格子
  2. 格子本身已经被打开 1)是:格子已经被标记为地雷? a. 是:取消标记(isMark和isOpen取反),剩余地雷数+1 b. 否:说明是右击了已经被点开的数字格,不做任何操作 2)否:isMark和isOpen取反,记录该格子已经被标记为地雷,格子处于打开状态,剩余地雷数-1,判断是否结束
// 右键确认是雷点
        handleSureMinePoint(lattice) {
            if (!lattice.isOpen) {
                lattice.isMark = true;
                lattice.isOpen = true;
                this.minePosition.splice(this.minePosition.indexOf(lattice.index), 1);
                this.judgeIsOver();
            } else {
                if (lattice.isMark) {
                    lattice.isMark = false;
                    lattice.isOpen = false;
                    this.minePosition.push(lattice.index);
                }
            }
        },

游戏是否结束

游戏结束一共有三种情况:1、点到雷了,直接结束 2、雷被标记完了(有可能失败了,标错了) 3、翻开的格子数 + 雷数 = 总格子数

完成功能

接下来要做的就是把格子属性隐藏起来,假装不知道,再假装鼠标一点,格子就翻过来了。这就用到之前提到的格子属性isMark和isOpen,本身元素处于隐藏状态,当被标记或者被打开的时候设置相应的属性使其可见就行了,如此,便完成了扫雷的基本功能,有兴趣的小朋友可以自己融合多种功能试一试

e6n2If3.jpg!web

当然,这只是其中一种实现方式,把所有的计算全部放在玩游戏之前了,爱动脑筋的朋友们也可以想想,如果放在每一次点击时做计算该如何组织代码

阿望的源代码中还集合了【重开一局】、【改变难度】、【游戏计时】等功能,样式兼容手机和PC在线玩,在手机上玩的时候我在纠结手机如何模仿PC端的右键点击标雷操作,没有好的想法,不想用双击,于是多加了一个状态,点击页面【标记】按钮,即表示标记雷点,再点击一次表示还原,正常点开数字格,坐火车无聊的小朋友可以玩一玩哟

查看阿望的源码: mineSweeping

在线试玩:mine-sweeping-online

希望各位看官不吝右上角赐个小星星哦,阿望这厢有礼啦,新年快乐啦 ★,° :.☆( ̄▽ ̄)/$: .°★

予人玫瑰,手有余香!如果本博的某篇文章帮助了你,并且让你觉得想要放到自己的博客上,欢迎转载,转载请注明出处。

为能够给自己带来价值的东西付费,是再正常不过的事情,物质支持 【右侧打赏】 和精神支持 【评论、转载】 都是我持续总结,持续分享的最大动力!

有兴趣的朋友可以关注我的公众号:非梧不栖oy,微信可直接搜索关注或是下方扫码关注,公众号会同步本站的部分博客,您可以直接在手机上随时阅读到自己感兴趣的文章

VJNBfyr.png!web

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK