话说阿望还在大学时,某一天寝室忽然停网了,因而和室友两人不约而同地打开了扫雷,比相同难度下谁更快找出所有的雷,玩得不亦乐乎,就这样,扫雷伴咱们度过了断网的一周,是整整一周啊,不用上课的那种,可想而知咱们是有多无聊了。html
这两天临近过年了,该放假的已经放假了,不应放假的已经请假了,公交不打挤了,地铁口不堵了,公司也去了大半部分的人了,就留阿望这种不得不留下来值班的人守着空荡荡的办公室了,因而,多年前那种无所事事的断网心态再次袭来,因而,想玩扫雷的心再次蹦跶出来,因而,点开了电脑的附件,因而,发现个人电脑上并无扫雷【手动微笑.jpg】。vue
怎么想起要写扫雷的阿望就很少废话了,直接开干。git
想当年阿望仍是在大学才参透扫雷的游戏规则的,初高中的时候都是靠运气瞎点,从没赢过,固然了,境界提高之后,追求的天然就不是赢了,而是速度。github
来,看规则:算法
大白话游戏规则,计算逻辑以下:vuex
好,这下规则咱们了解了,接下来,摩拳擦掌,开始写代码咯,本次代码用vue来写,没有缘由,习惯了而已。json
看着游戏规则,咱们先来理一理,要如何完成这个功能,咱们以最简单的,最能令人理解的步骤,来完成此次功能。数组
避免vue新手不知如何下手,那就从建项目开始吧,环境安装我就不讲了,脚手架也是要有的sass
第一步,初始化lovesweeping工程(阿望不喜欢地雷,喜欢小桃心)dom
项目很小,不须要路由,不须要vuex,便可完成,只带了sass,连eslint均可以不要,看官们能够根据本身的喜爱建项目
项目生成以后,helloworld就能够删掉了,它的存在并无什么意义
这一步主要是构建工程结构,简单画一下主要的几个文件
- src - components - SelectLevel.vue [新增,难度选择组件] - LoveSweeping.vue [新增,游戏界面组件] - App.vue [父组件,负责组件间的切换和某些数据传递] - main.js - package.json
好,初步认识了项目结构之后,咱把该新建的建好,能够不加东西,不报错就行
而后就是把组件间的切换代码写好,再来一步步的填充代码
这一步很简单,首先画好难度选择的页面,难度能够本身设置,你以为合理就行,我这里的数据格式是这样的,用一个对象表示一个难度等级,对象中包含了难度描述,以及难度设置,设置中包含了格子横排数,格子纵排数,雷数
// 难度 level: [ { text: '青铜', // 难度描述 value: [9, 9, 10] // 格子横排数,格子纵排数,雷数 }, { text: '黄金', value: [12, 9, 20] ]
而后模板中直接渲染列表,这样作的好处是,想要增长难度直接在数组中添加数据便可
<li v-for="(item, index) in level" :key="index" @click="handleChoseLevel(item.value)">{{ item.text }}</li>
该组件中只有一个方法:选择难度以后,跳转到游戏主界面上去,由于项目没有用路由,直接使用组件间的切换,因此,这个方法只负责告诉父组件,我已经选择好难度了,能够开始游戏了
// 选择难度 handleChoseLevel(level) { this.$emit("chose-level", level); }
代码以下:
界面长这样,固然,你要以为难看本身换个样式也行
经过游戏难度选择来决定游戏格盘的大小,组件间已经过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>{{(col - 1) * rows + row - 1}}</span> </span> </div>
首先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; },
把地雷位置暴露出来
确认了雷点位置,接下来要作的就是确认每个非雷点位置周围的雷的数量,咱们用对象来描述一个格子,这个对象主要包含如下几个属性
// 格子属性 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; },
有了这两个方法,咱成功地获取到了每一个非雷点格子周围的雷的数量,来,展现出来,这样展现的好处是,咱们一眼就能够看出算法是否正确
没问题了,来,接着往下走,格盘数据基本都设置好了,那咱们接下来要作的就是,点开格子操做
这一步先作简单点,有个明显的区别就能够了,点雷咱们先无论,先看点数字和空白的状况,首先得明确,到时候格子的可见属性是所有要被隐藏的,点击了才会显示出来,这就用到了咱们上一步提到的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; } } },
点空白
第二个难点来咯,点空白格须要注意如下几点:一、空白格表示周围8格都没有雷 二、扩散周围8格,判断雷数,循环往复 三、遇边界中止扩散,遇数字中止扩散
假设横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方法再加一步判断,若是是雷,游戏结束
这个就很简单了,写个右击事件,修改一下格子的isMark和isOpen属性,这一步的基本逻辑就是
// 右键确认是雷点 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); } } },
游戏结束一共有三种状况:一、点到雷了,直接结束 二、雷被标记完了(有可能失败了,标错了) 三、翻开的格子数 + 雷数 = 总格子数
接下来要作的就是把格子属性隐藏起来,伪装不知道,再伪装鼠标一点,格子就翻过来了。这就用到以前提到的格子属性isMark和isOpen,自己元素处于隐藏状态,当被标记或者被打开的时候设置相应的属性使其可见就好了,如此,便完成了扫雷的基本功能,有兴趣的小朋友能够本身融合多种功能试一试
固然,这只是其中一种实现方式,把全部的计算所有放在玩游戏以前了,爱动脑筋的朋友们也能够想一想,若是放在每一次点击时作计算该如何组织代码
阿望的源代码中还集合了【重开一局】、【改变难度】、【游戏计时】等功能,样式兼容手机和PC在线玩,在手机上玩的时候我在纠结手机如何模仿PC端的右键点击标雷操做,没有好的想法,不想用双击,因而多加了一个状态,点击页面【标记】按钮,即表示标记雷点,再点击一次表示还原,正常点开数字格,坐火车无聊的小朋友能够玩一玩哟
查看阿望的源码:mineSweeping
在线试玩:mine-sweeping-online
但愿各位看官不吝右上角赐个小星星哦,阿望这厢有礼啦,新年快乐啦★,°:.☆( ̄▽ ̄)/$:.°★ 。